Merge branch 'develop' into 'bookmark-folders'
# Conflicts: # docs/development/API/differences_in_mastoapi_responses.md
This commit is contained in:
commit
2b71f4897f
|
@ -169,25 +169,6 @@ unit-testing-1.12-erratic:
|
||||||
- mix ecto.migrate
|
- mix ecto.migrate
|
||||||
- mix test --only=erratic
|
- mix test --only=erratic
|
||||||
|
|
||||||
unit-testing-1.12-rum:
|
|
||||||
extends:
|
|
||||||
- .build_changes_policy
|
|
||||||
- .using-ci-base
|
|
||||||
stage: test
|
|
||||||
cache: *testing_cache_policy
|
|
||||||
services:
|
|
||||||
- name: git.pleroma.social:5050/pleroma/pleroma/postgres-with-rum-13
|
|
||||||
alias: postgres
|
|
||||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
|
||||||
variables:
|
|
||||||
<<: *global_variables
|
|
||||||
RUM_ENABLED: "true"
|
|
||||||
script:
|
|
||||||
- mix ecto.create
|
|
||||||
- mix ecto.migrate
|
|
||||||
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
|
|
||||||
- mix test --preload-modules
|
|
||||||
|
|
||||||
formatting-1.13:
|
formatting-1.13:
|
||||||
extends: .build_changes_policy
|
extends: .build_changes_policy
|
||||||
image: &formatting_elixir elixir:1.13-alpine
|
image: &formatting_elixir elixir:1.13-alpine
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Include following/followers in backups
|
|
@ -0,0 +1 @@
|
||||||
|
Add ForceMention MRF
|
|
@ -0,0 +1 @@
|
||||||
|
Video framegrabs were not working correctly after the change to use Exile to execute ffmpeg
|
|
@ -0,0 +1 @@
|
||||||
|
Connection pool errors when publishing an activity is a soft-error that will be retried shortly.
|
|
@ -0,0 +1 @@
|
||||||
|
Add contact account to InstanceView
|
|
@ -0,0 +1 @@
|
||||||
|
Handle cases when users.inbox is nil.
|
|
@ -0,0 +1 @@
|
||||||
|
Verify profile link ownership with rel="me"
|
|
@ -0,0 +1 @@
|
||||||
|
Notifications: improve performance by filtering on users table instead of activities table
|
|
@ -0,0 +1 @@
|
||||||
|
Expose nonAnonymous field from Smithereen polls
|
|
@ -415,6 +415,10 @@
|
||||||
|
|
||||||
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
|
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
|
||||||
|
|
||||||
|
config :pleroma, :mrf_force_mention,
|
||||||
|
mention_parent: true,
|
||||||
|
mention_quoted: true
|
||||||
|
|
||||||
config :pleroma, :rich_media,
|
config :pleroma, :rich_media,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
ignore_hosts: [],
|
ignore_hosts: [],
|
||||||
|
|
|
@ -566,6 +566,20 @@
|
||||||
"Cool instance"
|
"Cool instance"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :status_page,
|
||||||
|
type: :string,
|
||||||
|
description: "A page where people can see the status of the server during an outage",
|
||||||
|
suggestions: [
|
||||||
|
"https://status.pleroma.example.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :contact_username,
|
||||||
|
type: :string,
|
||||||
|
description: "Instance owner username",
|
||||||
|
suggestions: ["admin"]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :limit,
|
key: :limit,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
|
|
|
@ -161,7 +161,8 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
||||||
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
|
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
|
||||||
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
|
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
|
||||||
* `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
|
* `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions).
|
||||||
|
* `Pleroma.Web.ActivityPub.MRF.ForceMention`: Forces posts to include a mention of the author of parent post or the author of quoted post.
|
||||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||||
|
|
||||||
|
@ -272,6 +273,10 @@ Notes:
|
||||||
#### :mrf_inline_quote
|
#### :mrf_inline_quote
|
||||||
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `<bdi>RT:</bdi> {url}`
|
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `<bdi>RT:</bdi> {url}`
|
||||||
|
|
||||||
|
#### :mrf_force_mention
|
||||||
|
* `mention_parent`: Whether to append mention of parent post author
|
||||||
|
* `mention_quoted`: Whether to append mention of parent quoted author
|
||||||
|
|
||||||
### :activitypub
|
### :activitypub
|
||||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||||
|
|
|
@ -40,6 +40,7 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `parent_visible`: If the parent of this post is visible to the user or not.
|
- `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.
|
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
|
||||||
- `quotes_count`: the count of status quotes.
|
- `quotes_count`: the count of status quotes.
|
||||||
|
- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
|
||||||
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
|
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
|
||||||
|
|
||||||
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
|
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
|
||||||
|
|
|
@ -241,13 +241,13 @@ def find(following_relationships, follower, following) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
For a query with joined activity,
|
For a query with joined activity's actor,
|
||||||
keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
|
keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
|
||||||
"""
|
"""
|
||||||
def keep_following_or_not_domain_blocked(query, user) do
|
def keep_following_or_not_domain_blocked(query, user) do
|
||||||
where(
|
where(
|
||||||
query,
|
query,
|
||||||
[_, activity],
|
[_, user_actor: user_actor],
|
||||||
fragment(
|
fragment(
|
||||||
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
|
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
|
||||||
"""
|
"""
|
||||||
|
@ -255,9 +255,9 @@ def keep_following_or_not_domain_blocked(query, user) do
|
||||||
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
|
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
|
||||||
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
|
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
|
||||||
""",
|
""",
|
||||||
activity.actor,
|
user_actor.ap_id,
|
||||||
^user.domain_blocks,
|
^user.domain_blocks,
|
||||||
activity.actor,
|
user_actor.ap_id,
|
||||||
^User.binary_id(user.id),
|
^User.binary_id(user.id),
|
||||||
^accept_state_code()
|
^accept_state_code()
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,10 +18,10 @@ def init(_opts) do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_worker(opts, retry \\ false) do
|
def start_worker(opts, last_attempt \\ false) do
|
||||||
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
|
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
|
||||||
{:error, :max_children} ->
|
{:error, :max_children} ->
|
||||||
funs = [fn -> !retry end, fn -> match?(:error, free_pool()) end]
|
funs = [fn -> last_attempt end, fn -> match?(:error, free_pool()) end]
|
||||||
|
|
||||||
if Enum.any?(funs, fn fun -> fun.() end) do
|
if Enum.any?(funs, fn fun -> fun.() end) do
|
||||||
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
|
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
|
||||||
|
|
|
@ -40,28 +40,32 @@ def image_resize(url, options) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
||||||
|
@spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
|
||||||
def video_framegrab(url) do
|
def video_framegrab(url) do
|
||||||
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
||||||
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||||
{:ok, pid} <- StringIO.open(env.body) do
|
{:ok, pid} <- StringIO.open(env.body) do
|
||||||
body_stream = IO.binstream(pid, 1)
|
body_stream = IO.binstream(pid, 1)
|
||||||
|
|
||||||
Exile.stream!(
|
result =
|
||||||
[
|
Exile.stream!(
|
||||||
executable,
|
[
|
||||||
"-i",
|
executable,
|
||||||
"pipe:0",
|
"-i",
|
||||||
"-vframes",
|
"pipe:0",
|
||||||
"1",
|
"-vframes",
|
||||||
"-f",
|
"1",
|
||||||
"mjpeg",
|
"-f",
|
||||||
"pipe:1"
|
"mjpeg",
|
||||||
],
|
"pipe:1"
|
||||||
input: body_stream,
|
],
|
||||||
ignore_epipe: true,
|
input: body_stream,
|
||||||
stderr: :disable
|
ignore_epipe: true,
|
||||||
)
|
stderr: :disable
|
||||||
|> Enum.into(<<>>)
|
)
|
||||||
|
|> Enum.into(<<>>)
|
||||||
|
|
||||||
|
{:ok, result}
|
||||||
else
|
else
|
||||||
nil -> {:error, {:ffmpeg, :command_not_found}}
|
nil -> {:error, {:ffmpeg, :command_not_found}}
|
||||||
{:error, _} = error -> error
|
{:error, _} = error -> error
|
||||||
|
|
|
@ -137,7 +137,7 @@ defp exclude_blocked(query, user, opts) do
|
||||||
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> where([n, a], a.actor not in ^blocked_ap_ids)
|
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|
||||||
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
|
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ defp exclude_blockers(query, user) do
|
||||||
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
|
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
|
||||||
|
|
||||||
query
|
query
|
||||||
|> where([n, a], a.actor not in ^blocker_ap_ids)
|
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ defp exclude_notification_muted(query, user, opts) do
|
||||||
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
|
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> where([n, a], a.actor not in ^notification_muted_ap_ids)
|
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|
||||||
|> join(:left, [n, a], tm in ThreadMute,
|
|> join(:left, [n, a], tm in ThreadMute,
|
||||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
|
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
|
||||||
as: :thread_mute
|
as: :thread_mute
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.User do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Ecto, only: [assoc: 2]
|
import Ecto, only: [assoc: 2]
|
||||||
|
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||||
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
@ -596,9 +597,23 @@ def update_changeset(struct, params \\ %{}) do
|
||||||
|
|
||||||
defp put_fields(changeset) do
|
defp put_fields(changeset) do
|
||||||
if raw_fields = get_change(changeset, :raw_fields) do
|
if raw_fields = get_change(changeset, :raw_fields) do
|
||||||
|
old_fields = changeset.data.raw_fields
|
||||||
|
|
||||||
raw_fields =
|
raw_fields =
|
||||||
raw_fields
|
raw_fields
|
||||||
|> Enum.filter(fn %{"name" => n} -> n != "" end)
|
|> Enum.filter(fn %{"name" => n} -> n != "" end)
|
||||||
|
|> Enum.map(fn field ->
|
||||||
|
previous =
|
||||||
|
old_fields
|
||||||
|
|> Enum.find(fn %{"value" => value} -> field["value"] == value end)
|
||||||
|
|
||||||
|
if previous && Map.has_key?(previous, "verified_at") do
|
||||||
|
field
|
||||||
|
|> Map.put("verified_at", previous["verified_at"])
|
||||||
|
else
|
||||||
|
field
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
fields =
|
fields =
|
||||||
raw_fields
|
raw_fields
|
||||||
|
@ -1200,6 +1215,10 @@ def update_and_set_cache(struct, params) do
|
||||||
|
|
||||||
def update_and_set_cache(changeset) do
|
def update_and_set_cache(changeset) do
|
||||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||||
|
if get_change(changeset, :raw_fields) do
|
||||||
|
BackgroundWorker.enqueue("verify_fields_links", %{"user_id" => user.id})
|
||||||
|
end
|
||||||
|
|
||||||
set_cache(user)
|
set_cache(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1975,8 +1994,45 @@ def perform(:delete, %User{} = user) do
|
||||||
maybe_delete_from_db(user)
|
maybe_delete_from_db(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform(:verify_fields_links, user) do
|
||||||
|
profile_urls = [user.ap_id]
|
||||||
|
|
||||||
|
fields =
|
||||||
|
user.raw_fields
|
||||||
|
|> Enum.map(&verify_field_link(&1, profile_urls))
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
user
|
||||||
|
|> update_changeset(%{raw_fields: fields})
|
||||||
|
|
||||||
|
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||||
|
set_cache(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def perform(:set_activation_async, user, status), do: set_activation(user, status)
|
def perform(:set_activation_async, user, status), do: set_activation(user, status)
|
||||||
|
|
||||||
|
defp verify_field_link(field, profile_urls) do
|
||||||
|
verified_at =
|
||||||
|
with %{"value" => value} <- field,
|
||||||
|
{:verified_at, nil} <- {:verified_at, Map.get(field, "verified_at")},
|
||||||
|
%{scheme: scheme, userinfo: nil, host: host}
|
||||||
|
when not_empty_string(host) and scheme in ["http", "https"] <-
|
||||||
|
URI.parse(value),
|
||||||
|
{:not_idn, true} <- {:not_idn, to_string(:idna.encode(host)) == host},
|
||||||
|
"me" <- Pleroma.Web.RelMe.maybe_put_rel_me(value, profile_urls) do
|
||||||
|
CommonUtils.to_masto_date(NaiveDateTime.utc_now())
|
||||||
|
else
|
||||||
|
{:verified_at, value} when not_empty_string(value) ->
|
||||||
|
value
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
Map.put(field, "verified_at", verified_at)
|
||||||
|
end
|
||||||
|
|
||||||
@spec external_users_query() :: Ecto.Query.t()
|
@spec external_users_query() :: Ecto.Query.t()
|
||||||
def external_users_query do
|
def external_users_query do
|
||||||
User.Query.build(%{
|
User.Query.build(%{
|
||||||
|
@ -2664,10 +2720,11 @@ def sanitize_html(%User{} = user) do
|
||||||
# - display name
|
# - display name
|
||||||
def sanitize_html(%User{} = user, filter) do
|
def sanitize_html(%User{} = user, filter) do
|
||||||
fields =
|
fields =
|
||||||
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
|
Enum.map(user.fields, fn %{"name" => name, "value" => value} = fields ->
|
||||||
%{
|
%{
|
||||||
"name" => name,
|
"name" => name,
|
||||||
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
|
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly),
|
||||||
|
"verified_at" => Map.get(fields, "verified_at")
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
@ -196,7 +196,14 @@ defp wait_backup(backup, current_processed, task) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
|
@files [
|
||||||
|
'actor.json',
|
||||||
|
'outbox.json',
|
||||||
|
'likes.json',
|
||||||
|
'bookmarks.json',
|
||||||
|
'followers.json',
|
||||||
|
'following.json'
|
||||||
|
]
|
||||||
@spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error
|
@spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error
|
||||||
def export(%__MODULE__{} = backup, caller_pid) do
|
def export(%__MODULE__{} = backup, caller_pid) do
|
||||||
backup = Repo.preload(backup, :user)
|
backup = Repo.preload(backup, :user)
|
||||||
|
@ -207,6 +214,8 @@ def export(%__MODULE__{} = backup, caller_pid) do
|
||||||
:ok <- statuses(dir, backup.user, caller_pid),
|
:ok <- statuses(dir, backup.user, caller_pid),
|
||||||
:ok <- likes(dir, backup.user, caller_pid),
|
:ok <- likes(dir, backup.user, caller_pid),
|
||||||
:ok <- bookmarks(dir, backup.user, caller_pid),
|
:ok <- bookmarks(dir, backup.user, caller_pid),
|
||||||
|
:ok <- followers(dir, backup.user, caller_pid),
|
||||||
|
:ok <- following(dir, backup.user, caller_pid),
|
||||||
{:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir),
|
{:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir),
|
||||||
{:ok, _} <- File.rm_rf(dir) do
|
{:ok, _} <- File.rm_rf(dir) do
|
||||||
{:ok, zip_path}
|
{:ok, zip_path}
|
||||||
|
@ -357,6 +366,16 @@ defp statuses(dir, user, caller_pid) do
|
||||||
caller_pid
|
caller_pid
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp followers(dir, user, caller_pid) do
|
||||||
|
User.get_followers_query(user)
|
||||||
|
|> write(dir, "followers", fn a -> {:ok, a.ap_id} end, caller_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp following(dir, user, caller_pid) do
|
||||||
|
User.get_friends_query(user)
|
||||||
|
|> write(dir, "following", fn a -> {:ok, a.ap_id} end, caller_pid)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Pleroma.User.Backup.ProcessorAPI do
|
defmodule Pleroma.User.Backup.ProcessorAPI do
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
|
||||||
|
defp get_author(url) do
|
||||||
|
with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
|
||||||
|
%User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
|
||||||
|
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepend_author(tags, _, false), do: tags
|
||||||
|
|
||||||
|
defp prepend_author(tags, nil, _), do: tags
|
||||||
|
|
||||||
|
defp prepend_author(tags, url, _) do
|
||||||
|
actor = get_author(url)
|
||||||
|
|
||||||
|
if not is_nil(actor) do
|
||||||
|
[actor | tags]
|
||||||
|
else
|
||||||
|
tags
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
|
||||||
|
tag =
|
||||||
|
tag
|
||||||
|
|> prepend_author(
|
||||||
|
object["inReplyTo"],
|
||||||
|
Config.get([:mrf_force_mention, :mention_parent, true])
|
||||||
|
)
|
||||||
|
|> prepend_author(
|
||||||
|
object["quoteUrl"],
|
||||||
|
Config.get([:mrf_force_mention, :mention_quoted, true])
|
||||||
|
)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
|
{:ok, put_in(activity["object"]["tag"], tag)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def filter(object), do: {:ok, object}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def describe, do: {:ok, %{}}
|
||||||
|
end
|
|
@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
||||||
|
|
||||||
field(:closed, ObjectValidators.DateTime)
|
field(:closed, ObjectValidators.DateTime)
|
||||||
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
||||||
|
field(:nonAnonymous, :boolean)
|
||||||
embeds_many(:anyOf, QuestionOptionsValidator)
|
embeds_many(:anyOf, QuestionOptionsValidator)
|
||||||
embeds_many(:oneOf, QuestionOptionsValidator)
|
embeds_many(:oneOf, QuestionOptionsValidator)
|
||||||
end
|
end
|
||||||
|
|
|
@ -129,6 +129,10 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
||||||
_ -> {:error, e}
|
_ -> {:error, e}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
{:error, :pool_full} ->
|
||||||
|
Logger.debug("Publisher snoozing worker job due to full connection pool")
|
||||||
|
{:snooze, 30}
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
||||||
Logger.metadata(activity: id, inbox: inbox)
|
Logger.metadata(activity: id, inbox: inbox)
|
||||||
|
@ -154,19 +158,18 @@ defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp should_federate?(inbox, public) do
|
def should_federate?(nil, _), do: false
|
||||||
if public do
|
def should_federate?(_, true), do: true
|
||||||
true
|
|
||||||
else
|
|
||||||
%{host: host} = URI.parse(inbox)
|
|
||||||
|
|
||||||
quarantined_instances =
|
def should_federate?(inbox, _) do
|
||||||
Config.get([:instance, :quarantined_instances], [])
|
%{host: host} = URI.parse(inbox)
|
||||||
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
|
||||||
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
|
||||||
|
|
||||||
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
|
quarantined_instances =
|
||||||
end
|
Config.get([:instance, :quarantined_instances], [])
|
||||||
|
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
||||||
|
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
||||||
|
|
||||||
|
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
|
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
|
||||||
|
|
|
@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: "Possible answers for the poll."
|
description: "Possible answers for the poll."
|
||||||
|
},
|
||||||
|
pleroma: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
non_anonymous: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Can voters be publicly identified?"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
|
@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
||||||
votes_count: 4
|
votes_count: 4
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
emojis: []
|
emojis: [],
|
||||||
|
pleroma: %{
|
||||||
|
non_anonymous: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,7 @@ def render("show.json", _) do
|
||||||
|> to_string,
|
|> to_string,
|
||||||
registrations: Keyword.get(instance, :registrations_open),
|
registrations: Keyword.get(instance, :registrations_open),
|
||||||
approval_required: Keyword.get(instance, :account_approval_required),
|
approval_required: Keyword.get(instance, :account_approval_required),
|
||||||
|
contact_account: contact_account(Keyword.get(instance, :contact_username)),
|
||||||
configuration: configuration(),
|
configuration: configuration(),
|
||||||
# Extra (not present in Mastodon):
|
# Extra (not present in Mastodon):
|
||||||
max_toot_chars: Keyword.get(instance, :limit),
|
max_toot_chars: Keyword.get(instance, :limit),
|
||||||
|
@ -63,11 +64,12 @@ def render("show2.json", _) do
|
||||||
registrations: %{
|
registrations: %{
|
||||||
enabled: Keyword.get(instance, :registrations_open),
|
enabled: Keyword.get(instance, :registrations_open),
|
||||||
approval_required: Keyword.get(instance, :account_approval_required),
|
approval_required: Keyword.get(instance, :account_approval_required),
|
||||||
message: nil
|
message: nil,
|
||||||
|
url: nil
|
||||||
},
|
},
|
||||||
contact: %{
|
contact: %{
|
||||||
email: Keyword.get(instance, :email),
|
email: Keyword.get(instance, :email),
|
||||||
account: nil
|
account: contact_account(Keyword.get(instance, :contact_username))
|
||||||
},
|
},
|
||||||
# Extra (not present in Mastodon):
|
# Extra (not present in Mastodon):
|
||||||
pleroma: pleroma_configuration2(instance)
|
pleroma: pleroma_configuration2(instance)
|
||||||
|
@ -78,7 +80,8 @@ defp common_information(instance) do
|
||||||
%{
|
%{
|
||||||
title: Keyword.get(instance, :name),
|
title: Keyword.get(instance, :name),
|
||||||
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
|
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
|
||||||
languages: Keyword.get(instance, :languages, ["en"])
|
languages: Keyword.get(instance, :languages, ["en"]),
|
||||||
|
rules: []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -168,15 +171,35 @@ defp fields_limits do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp contact_account(nil), do: nil
|
||||||
|
|
||||||
|
defp contact_account("@" <> username) do
|
||||||
|
contact_account(username)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp contact_account(username) do
|
||||||
|
user = Pleroma.User.get_cached_by_nickname(username)
|
||||||
|
|
||||||
|
if user do
|
||||||
|
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp configuration do
|
defp configuration do
|
||||||
%{
|
%{
|
||||||
|
accounts: %{
|
||||||
|
max_featured_tags: 0
|
||||||
|
},
|
||||||
statuses: %{
|
statuses: %{
|
||||||
max_characters: Config.get([:instance, :limit]),
|
max_characters: Config.get([:instance, :limit]),
|
||||||
max_media_attachments: Config.get([:instance, :max_media_attachments])
|
max_media_attachments: Config.get([:instance, :max_media_attachments])
|
||||||
},
|
},
|
||||||
media_attachments: %{
|
media_attachments: %{
|
||||||
image_size_limit: Config.get([:instance, :upload_limit]),
|
image_size_limit: Config.get([:instance, :upload_limit]),
|
||||||
video_size_limit: Config.get([:instance, :upload_limit])
|
video_size_limit: Config.get([:instance, :upload_limit]),
|
||||||
|
supported_mime_types: ["application/octet-stream"]
|
||||||
},
|
},
|
||||||
polls: %{
|
polls: %{
|
||||||
max_options: Config.get([:instance, :poll_limits, :max_options]),
|
max_options: Config.get([:instance, :poll_limits, :max_options]),
|
||||||
|
@ -190,7 +213,13 @@ defp configuration do
|
||||||
defp configuration2 do
|
defp configuration2 do
|
||||||
configuration()
|
configuration()
|
||||||
|> Map.merge(%{
|
|> Map.merge(%{
|
||||||
urls: %{streaming: Pleroma.Web.Endpoint.websocket_url()}
|
urls: %{
|
||||||
|
streaming: Pleroma.Web.Endpoint.websocket_url(),
|
||||||
|
status: Config.get([:instance, :status_page])
|
||||||
|
},
|
||||||
|
vapid: %{
|
||||||
|
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,10 @@ def render("show.json", %{object: object, multiple: multiple, options: options}
|
||||||
votes_count: votes_count,
|
votes_count: votes_count,
|
||||||
voters_count: voters_count(object),
|
voters_count: voters_count(object),
|
||||||
options: options,
|
options: options,
|
||||||
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]),
|
||||||
|
pleroma: %{
|
||||||
|
non_anonymous: object.data["nonAnonymous"] || false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if params[:for] do
|
if params[:for] do
|
||||||
|
|
|
@ -40,6 +40,11 @@ def perform(%Job{
|
||||||
Pleroma.FollowingRelationship.move_following(origin, target)
|
Pleroma.FollowingRelationship.move_following(origin, target)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform(%Job{args: %{"op" => "verify_fields_links", "user_id" => user_id}}) do
|
||||||
|
user = User.get_by_id(user_id)
|
||||||
|
User.perform(:verify_fields_links, user)
|
||||||
|
end
|
||||||
|
|
||||||
def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
|
def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
|
||||||
Instance.perform(:delete_instance, host)
|
Instance.perform(:delete_instance, host)
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,11 @@ def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
|
||||||
{:error, :allowed_depth} ->
|
{:error, :allowed_depth} ->
|
||||||
{:discard, :allowed_depth}
|
{:discard, :allowed_depth}
|
||||||
|
|
||||||
_ ->
|
{:error, _} = e ->
|
||||||
:error
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
4
mix.exs
4
mix.exs
|
@ -186,8 +186,8 @@ defp deps do
|
||||||
{:elixir_make, "~> 0.7.7", override: true},
|
{:elixir_make, "~> 0.7.7", override: true},
|
||||||
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
|
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
|
||||||
{:exile,
|
{:exile,
|
||||||
git: "https://git.pleroma.social/pleroma/elixir-libraries/exile.git",
|
git: "https://github.com/akash-akya/exile.git",
|
||||||
ref: "0d6337cf68e7fbc8a093cae000955aa93b067f91"},
|
ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"},
|
||||||
{:bandit, "~> 1.2"},
|
{:bandit, "~> 1.2"},
|
||||||
|
|
||||||
## dev & test
|
## dev & test
|
||||||
|
|
2
mix.lock
2
mix.lock
|
@ -47,7 +47,7 @@
|
||||||
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
|
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
|
||||||
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
|
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
|
||||||
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
|
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
|
||||||
"exile": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/exile.git", "0d6337cf68e7fbc8a093cae000955aa93b067f91", [ref: "0d6337cf68e7fbc8a093cae000955aa93b067f91"]},
|
"exile": {:git, "https://github.com/akash-akya/exile.git", "be87c33b02a7c3c5d22d2ece01fbd462355b28ef", [ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"]},
|
||||||
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
|
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
|
||||||
"fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"},
|
"fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"},
|
||||||
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
|
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
|
||||||
|
|
|
@ -40,7 +40,9 @@
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
},
|
},
|
||||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||||
"formerRepresentations": "litepub:formerRepresentations"
|
"formerRepresentations": "litepub:formerRepresentations",
|
||||||
|
"sm": "http://smithereen.software/ns#",
|
||||||
|
"nonAnonymous": "sm:nonAnonymous"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}}
|
|
@ -0,0 +1 @@
|
||||||
|
{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
|
|
@ -0,0 +1 @@
|
||||||
|
{"type":"Question","id":"https://friends.grishka.me/posts/54642","attributedTo":"https://friends.grishka.me/users/1","content":"<p>здесь тоже можно что-то написать отдельно от опроса</p>","published":"2021-09-04T00:22:16Z","url":"https://friends.grishka.me/posts/54642","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://friends.grishka.me/users/1/followers"],"replies":{"type":"Collection","id":"https://friends.grishka.me/posts/54642/replies","first":{"type":"CollectionPage","items":[],"partOf":"https://friends.grishka.me/posts/54642/replies","next":"https://friends.grishka.me/posts/54642/replies?page=1"}},"sensitive":false,"likes":"https://friends.grishka.me/posts/54642/likes","name":"тестовый опрос","oneOf":[{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/76","name":"тестовый ответ 1","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/76/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/77","name":"тестовый ответ 2","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/77/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/78","name":"тестовый ответ 3","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/78/votes","totalItems":6,"items":[]}}],"votersCount":14,"nonAnonymous":true,"@context":["https://www.w3.org/ns/activitystreams",{"sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#","sm":"http://smithereen.software/ns#","votersCount":"toot:votersCount","nonAnonymous":"sm:nonAnonymous"}]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"type":"Person","id":"https://friends.grishka.me/users/1","name":"Григорий Клюшников","icon":{"type":"Image","image":{"type":"Image","url":"https://friends.grishka.me/i/6QLsOws97AWp5N_osd74C1IC1ijnFopyCBD9MSEeXNQ/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg","width":1280,"height":960},"width":573,"height":572,"cropRegion":[0.26422762870788574,0.3766937553882599,0.7113820910453796,0.9728997349739075],"url":"https://friends.grishka.me/i/ql_49PQcETAWgY_nC-Qj63H_Oa6FyOAEoWFkUSSkUvQ/c:573:572:nowe:338:362/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg"},"summary":"<p>Делаю эту хрень, пытаюсь вырвать социальные сети из жадных лап корпораций</p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p>This server does NOT support direct messages. Please write me <a href=\"https://t.me/grishka\">on Telegram</a> or <a href=\"https://matrix.to/#/@grishk:matrix.org\">Matrix</a>.</p>","url":"https://friends.grishka.me/grishka","preferredUsername":"grishka","inbox":"https://friends.grishka.me/users/1/inbox","outbox":"https://friends.grishka.me/users/1/outbox","followers":"https://friends.grishka.me/users/1/followers","following":"https://friends.grishka.me/users/1/following","endpoints":{"sharedInbox":"https://friends.grishka.me/activitypub/sharedInbox","collectionSimpleQuery":"https://friends.grishka.me/users/1/collectionQuery"},"publicKey":{"id":"https://friends.grishka.me/users/1#main-key","owner":"https://friends.grishka.me/users/1","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjlakm+i/d9ER/hIeR7KfiFW+SdLZj2SkKIeM8cmR+YFJuh9ghFqXrkFEjcaqUnAFqe5gYDNSQACnDLA8y4DnzjfGNIohKAnRoa9x6GORmfKQvcnjaTZ53S1NvUiPPyc0Pv/vfCtY/Ab0CEXe5BLqL38oZn817Jf7pBrPRTYH7m012kvwAUTT6k0Y8lPITBEG7nzYbbuGcrN9Y/RDdwE08jmBXlZ45bahRH3VNXVpQE17dCzJB+7k+iJ1R7YCoI+DuMlBYGXGE2KVk46NZTuLnOjFV9SyXfWX4/SrJM4oxev+SX2N75tQgmNZmVVHeqg2ZcbC0WCfNjJOi2HHS9MujwIDAQAB\n-----END PUBLIC KEY-----\n"},"wall":"https://friends.grishka.me/users/1/wall","firstName":"Григорий","lastName":"Клюшников","middleName":"Александрович","vcard:bday":"1993-01-22","gender":"http://schema.org#Male","supportsFriendRequests":true,"friends":"https://friends.grishka.me/users/1/friends","groups":"https://friends.grishka.me/users/1/groups","capabilities":{"supportsFriendRequests":true},"@context":["https://www.w3.org/ns/activitystreams",{"sm":"http://smithereen.software/ns#","cropRegion":{"@id":"sm:cropRegion","@container":"@list"},"wall":{"@id":"sm:wall","@type":"@id"},"collectionSimpleQuery":"sm:collectionSimpleQuery","sc":"http://schema.org#","firstName":"sc:givenName","lastName":"sc:familyName","middleName":"sc:additionalName","gender":{"@id":"sc:gender","@type":"sc:GenderType"},"maidenName":"sm:maidenName","friends":{"@id":"sm:friends","@type":"@id"},"groups":{"@id":"sm:groups","@type":"@id"},"vcard":"http://www.w3.org/2006/vcard/ns#","capabilities":"litepub:capabilities","supportsFriendRequests":"sm:supportsFriendRequests","litepub":"http://litepub.social/ns#"},"https://w3id.org/security/v1"]}
|
|
@ -166,6 +166,7 @@ test "it removes outdated backups after creating a fresh one" do
|
||||||
|
|
||||||
test "it creates a zip archive with user data" do
|
test "it creates a zip archive with user data" do
|
||||||
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
|
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
|
||||||
|
%{ap_id: other_ap_id} = other_user = insert(:user)
|
||||||
|
|
||||||
{:ok, %{object: %{data: %{"id" => id1}}} = status1} =
|
{:ok, %{object: %{data: %{"id" => id1}}} = status1} =
|
||||||
CommonAPI.post(user, %{status: "status1"})
|
CommonAPI.post(user, %{status: "status1"})
|
||||||
|
@ -182,6 +183,8 @@ test "it creates a zip archive with user data" do
|
||||||
Bookmark.create(user.id, status2.id)
|
Bookmark.create(user.id, status2.id)
|
||||||
Bookmark.create(user.id, status3.id)
|
Bookmark.create(user.id, status3.id)
|
||||||
|
|
||||||
|
CommonAPI.follow(user, other_user)
|
||||||
|
|
||||||
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||||
assert {:ok, path} = Backup.export(backup, self())
|
assert {:ok, path} = Backup.export(backup, self())
|
||||||
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
|
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
|
||||||
|
@ -261,6 +264,16 @@ test "it creates a zip archive with user data" do
|
||||||
"type" => "OrderedCollection"
|
"type" => "OrderedCollection"
|
||||||
} = Jason.decode!(json)
|
} = Jason.decode!(json)
|
||||||
|
|
||||||
|
assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id" => "following.json",
|
||||||
|
"orderedItems" => [^other_ap_id],
|
||||||
|
"totalItems" => 1,
|
||||||
|
"type" => "OrderedCollection"
|
||||||
|
} = Jason.decode!(json)
|
||||||
|
|
||||||
:zip.zip_close(zipfile)
|
:zip.zip_close(zipfile)
|
||||||
File.rm!(path)
|
File.rm!(path)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2928,4 +2928,51 @@ test "it doesn't pin users you do not follow" do
|
||||||
refute User.endorses?(user, pinned_user)
|
refute User.endorses?(user, pinned_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it checks fields links for a backlink" do
|
||||||
|
user = insert(:user, ap_id: "https://social.example.org/users/lain")
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
%{"name" => "Link", "value" => "http://example.com/rel_me/null"},
|
||||||
|
%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"},
|
||||||
|
%{"name" => "Not a link", "value" => "i'm not a link"}
|
||||||
|
]
|
||||||
|
|
||||||
|
user
|
||||||
|
|> User.update_and_set_cache(%{raw_fields: fields})
|
||||||
|
|
||||||
|
ObanHelpers.perform_all()
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{"verified_at" => nil},
|
||||||
|
%{"verified_at" => verified_at},
|
||||||
|
%{"verified_at" => nil}
|
||||||
|
] = user.fields
|
||||||
|
|
||||||
|
assert is_binary(verified_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating fields does not invalidate previously validated links" do
|
||||||
|
user = insert(:user, ap_id: "https://social.example.org/users/lain")
|
||||||
|
|
||||||
|
user
|
||||||
|
|> User.update_and_set_cache(%{
|
||||||
|
raw_fields: [%{"name" => "verified link", "value" => "http://example.com/rel_me/link"}]
|
||||||
|
})
|
||||||
|
|
||||||
|
ObanHelpers.perform_all()
|
||||||
|
|
||||||
|
%User{fields: [%{"verified_at" => verified_at}]} = user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
user
|
||||||
|
|> User.update_and_set_cache(%{
|
||||||
|
raw_fields: [%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}]
|
||||||
|
})
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
assert [%{"verified_at" => ^verified_at}] = user.fields
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionTest do
|
||||||
|
use Pleroma.DataCase
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
alias Pleroma.Web.ActivityPub.MRF.ForceMention
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "adds mention to a reply" do
|
||||||
|
lain =
|
||||||
|
insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false)
|
||||||
|
|
||||||
|
niobleoum =
|
||||||
|
insert(:user,
|
||||||
|
ap_id: "https://www.minds.com/api/activitypub/users/1198929502760083472",
|
||||||
|
nickname: "niobleoum@minds.com",
|
||||||
|
local: false
|
||||||
|
)
|
||||||
|
|
||||||
|
status = File.read!("test/fixtures/minds-pleroma-mentioned-post.json") |> Jason.decode!()
|
||||||
|
|
||||||
|
status_activity = %{
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => lain.ap_id,
|
||||||
|
"object" => status
|
||||||
|
}
|
||||||
|
|
||||||
|
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
|
||||||
|
|
||||||
|
reply = File.read!("test/fixtures/minds-invalid-mention-post.json") |> Jason.decode!()
|
||||||
|
|
||||||
|
reply_activity = %{
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => niobleoum.ap_id,
|
||||||
|
"object" => reply
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(reply_activity)
|
||||||
|
|
||||||
|
assert Enum.find(tag, fn %{"href" => href} -> href == lain.ap_id end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adds mention to a quote" do
|
||||||
|
user1 = insert(:user, ap_id: "https://misskey.io/users/83ssedkv53")
|
||||||
|
user2 = insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
|
||||||
|
|
||||||
|
status = File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json") |> Jason.decode!()
|
||||||
|
|
||||||
|
status_activity = %{
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => user1.ap_id,
|
||||||
|
"object" => status
|
||||||
|
}
|
||||||
|
|
||||||
|
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
|
||||||
|
|
||||||
|
quote_post = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
|
||||||
|
|
||||||
|
quote_activity = %{
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => user2.ap_id,
|
||||||
|
"object" => quote_post
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(quote_activity)
|
||||||
|
|
||||||
|
assert Enum.find(tag, fn %{"href" => href} -> href == user1.ap_id end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -25,6 +25,17 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
||||||
|
|
||||||
setup_all do: clear_config([:instance, :federating], true)
|
setup_all do: clear_config([:instance, :federating], true)
|
||||||
|
|
||||||
|
describe "should_federate?/1" do
|
||||||
|
test "it returns false when the inbox is nil" do
|
||||||
|
refute Publisher.should_federate?(nil, false)
|
||||||
|
refute Publisher.should_federate?(nil, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns true when public is true" do
|
||||||
|
assert Publisher.should_federate?(false, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "gather_webfinger_links/1" do
|
describe "gather_webfinger_links/1" do
|
||||||
test "it returns links" do
|
test "it returns links" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
@ -205,6 +216,7 @@ test "publish to url with with different ports" do
|
||||||
refute called(Instances.set_reachable(inbox))
|
refute called(Instances.set_reachable(inbox))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag capture_log: true
|
||||||
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
|
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
|
||||||
Instances,
|
Instances,
|
||||||
[:passthrough],
|
[:passthrough],
|
||||||
|
|
|
@ -107,6 +107,18 @@ test "instance languages", %{conn: conn} do
|
||||||
|> json_response_and_validate_schema(200)
|
|> json_response_and_validate_schema(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "get instance contact information", %{conn: conn} do
|
||||||
|
user = insert(:user, %{local: true})
|
||||||
|
|
||||||
|
clear_config([:instance, :contact_username], user.nickname)
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/instance")
|
||||||
|
|
||||||
|
assert result = json_response_and_validate_schema(conn, 200)
|
||||||
|
|
||||||
|
assert result["contact_account"]["id"] == user.id
|
||||||
|
end
|
||||||
|
|
||||||
test "get instance information v2", %{conn: conn} do
|
test "get instance information v2", %{conn: conn} do
|
||||||
clear_config([:auth, :oauth_consumer_strategies], [])
|
clear_config([:auth, :oauth_consumer_strategies], [])
|
||||||
|
|
||||||
|
|
|
@ -511,10 +511,15 @@ test "update fields", %{conn: conn} do
|
||||||
|> json_response_and_validate_schema(200)
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
assert account_data["fields"] == [
|
assert account_data["fields"] == [
|
||||||
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
|
%{
|
||||||
|
"name" => "<a href=\"http://google.com\">foo</a>",
|
||||||
|
"value" => "bar",
|
||||||
|
"verified_at" => nil
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "link.io",
|
"name" => "link.io",
|
||||||
"value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)
|
"value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>),
|
||||||
|
"verified_at" => nil
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -573,8 +578,8 @@ test "emojis in fields labels", %{conn: conn} do
|
||||||
|> json_response_and_validate_schema(200)
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
assert account_data["fields"] == [
|
assert account_data["fields"] == [
|
||||||
%{"name" => ":firefox:", "value" => "is best 2hu"},
|
%{"name" => ":firefox:", "value" => "is best 2hu", "verified_at" => nil},
|
||||||
%{"name" => "they wins", "value" => ":blank:"}
|
%{"name" => "they wins", "value" => ":blank:", "verified_at" => nil}
|
||||||
]
|
]
|
||||||
|
|
||||||
assert account_data["source"]["fields"] == [
|
assert account_data["source"]["fields"] == [
|
||||||
|
@ -602,10 +607,11 @@ test "update fields via x-www-form-urlencoded", %{conn: conn} do
|
||||||
|> json_response_and_validate_schema(200)
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
assert account["fields"] == [
|
assert account["fields"] == [
|
||||||
%{"name" => "foo", "value" => "bar"},
|
%{"name" => "foo", "value" => "bar", "verified_at" => nil},
|
||||||
%{
|
%{
|
||||||
"name" => "link",
|
"name" => "link",
|
||||||
"value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>)
|
"value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>),
|
||||||
|
"verified_at" => nil
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -627,7 +633,7 @@ test "update fields with empty name", %{conn: conn} do
|
||||||
|> json_response_and_validate_schema(200)
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
assert account["fields"] == [
|
assert account["fields"] == [
|
||||||
%{"name" => "foo", "value" => ""}
|
%{"name" => "foo", "value" => "", "verified_at" => nil}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,8 @@ test "renders a poll" do
|
||||||
%{title: "why are you even asking?", votes_count: 0}
|
%{title: "why are you even asking?", votes_count: 0}
|
||||||
],
|
],
|
||||||
votes_count: 0,
|
votes_count: 0,
|
||||||
voters_count: 0
|
voters_count: 0,
|
||||||
|
pleroma: %{non_anonymous: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = PollView.render("show.json", %{object: object})
|
result = PollView.render("show.json", %{object: object})
|
||||||
|
@ -165,4 +166,11 @@ test "doesn't strips HTML tags" do
|
||||||
]
|
]
|
||||||
} = PollView.render("show.json", %{object: object})
|
} = PollView.render("show.json", %{object: object})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "that poll is non anonymous" do
|
||||||
|
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
|
||||||
|
result = PollView.render("show.json", %{object: object})
|
||||||
|
|
||||||
|
assert result[:pleroma][:non_anonymous] == true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1503,6 +1503,24 @@ def get("https://example.com/empty", _, _, _) do
|
||||||
{:ok, %Tesla.Env{status: 200, body: "hello"}}
|
{:ok, %Tesla.Env{status: 200, body: "hello"}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get("https://friends.grishka.me/posts/54642", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
|
||||||
|
headers: activitypub_object_headers()
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://friends.grishka.me/users/1", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
|
||||||
|
headers: activitypub_object_headers()
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
def get(url, query, body, headers) do
|
def get(url, query, body, headers) do
|
||||||
{:error,
|
{:error,
|
||||||
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
|
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
|
||||||
|
|
Loading…
Reference in New Issue