Varied selection of Pleroma cherry-picks #567

Merged
floatingghost merged 20 commits from XxXCertifiedForkliftDriverXxX/akkoma:cherry-picks into develop 2023-07-27 12:53:57 +00:00
27 changed files with 549 additions and 86 deletions

View file

@ -21,7 +21,7 @@ defp generate_topics(object, activity) do
["user", "list"] ++ visibility_tags(object, activity) ["user", "list"] ++ visibility_tags(object, activity)
end 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 case Visibility.get_visibility(activity) do
"public" -> "public" ->
if activity.local do if activity.local do
@ -31,6 +31,10 @@ defp visibility_tags(object, activity) do
end end
|> item_creation_tags(object, activity) |> item_creation_tags(object, activity)
"local" ->
["public:local"]
|> item_creation_tags(object, activity)
"direct" -> "direct" ->
["direct"] ["direct"]
@ -39,6 +43,10 @@ defp visibility_tags(object, activity) do
end end
end end
defp visibility_tags(_object, _activity) do
[]
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++ tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) 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(%{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), defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]

View file

@ -195,6 +195,7 @@ defp exclude_filtered(query, user) do
from([_n, a, o] in query, from([_n, a, o] in query,
where: where:
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
fragment("?->>'content' is null", o.data) or
fragment("?->>'actor' = ?", o.data, ^user.ap_id) fragment("?->>'actor' = ?", o.data, ^user.ap_id)
) )
end end
@ -695,7 +696,7 @@ def skip?(
cond do cond do
opts[:type] == "poll" -> false opts[:type] == "poll" -> false
user.ap_id == actor -> false user.ap_id == actor -> false
!User.following?(follower, user) -> true !User.following?(user, follower) -> true
true -> false true -> false
end end
end end

View file

@ -876,7 +876,7 @@ def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
end end
end end
defp send_user_approval_email(user) do defp send_user_approval_email(%User{email: email} = user) when is_binary(email) do
user user
|> Pleroma.Emails.UserEmail.approval_pending_email() |> Pleroma.Emails.UserEmail.approval_pending_email()
|> Pleroma.Emails.Mailer.deliver_async() |> Pleroma.Emails.Mailer.deliver_async()
@ -884,6 +884,10 @@ defp send_user_approval_email(user) do
{:ok, :enqueued} {:ok, :enqueued}
end end
defp send_user_approval_email(_user) do
{:ok, :skipped}
end
defp send_admin_approval_emails(user) do defp send_admin_approval_emails(user) do
all_superusers() all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end) |> Enum.filter(fn user -> not is_nil(user.email) end)

View file

@ -22,7 +22,10 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba
end end
def fix_object_defaults(data) do 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"]) %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])

View file

@ -410,7 +410,7 @@ def blocks_operation do
operationId: "AccountController.blocks", operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}", description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}], security: [%{"oAuth" => ["read:blocks"]}],
parameters: pagination_params(), parameters: [with_relationships_param() | pagination_params()],
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_accounts())
} }

View file

@ -143,7 +143,7 @@ def admin_account do
} }
}, },
tags: %Schema{type: :string}, tags: %Schema{type: :string},
is_confirmed: %Schema{type: :string} is_confirmed: %Schema{type: :boolean}
} }
} }
end end

View file

@ -88,7 +88,7 @@ def reject_follow_request(follower, followed) do
def delete(activity_id, user) do def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- 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, _} <- {_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, fetch: false), activity}, {:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],

View file

@ -144,6 +144,8 @@ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do when is_list(options) do
limits = Config.get([:instance, :poll_limits]) limits = Config.get([:instance, :poll_limits])
options = options |> Enum.uniq()
with :ok <- validate_poll_expiration(expires_in, limits), with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits), :ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do :ok <- validate_poll_options_length(options, limits) do
@ -179,9 +181,14 @@ def make_poll_data(_data) do
end end
defp validate_poll_options_amount(options, %{max_options: max_options}) do defp validate_poll_options_amount(options, %{max_options: max_options}) do
if Enum.count(options) > max_options do 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"} {:error, "Poll can't contain more than #{max_options} options"}
else
true ->
:ok :ok
end end
end end

View file

@ -518,7 +518,12 @@ def blocks(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(users) |> 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 end
@doc "GET /api/v1/accounts/lookup" @doc "GET /api/v1/accounts/lookup"

View file

@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
@impl Provider @impl Provider
def build_tags(%{user: user}) do 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(profile_tree, "link[rel~=me]", "href") ++
Floki.attribute(bio_tree, "a[rel~=me]", "href")) Floki.attribute(profile_tree, "a[rel~=me]", "href"))
|> Enum.map(fn link -> |> Enum.map(fn link ->
{:link, [rel: "me", href: link], []} {:link, [rel: "me", href: link], []}
end) end)
end end
defp append_fields_tag(bio, fields) do
fields
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
end
end end

View file

@ -20,12 +20,12 @@ def build_tags(%{activity_id: id, object: object, user: user}) do
[ [
title_tag(user), 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 if attachments == [] or Metadata.activity_nsfw?(object) do
[ [
image_tag(user), image_tag(user),
{:meta, [property: "twitter:card", content: "summary"], []} {:meta, [name: "twitter:card", content: "summary"], []}
] ]
else else
attachments attachments
@ -37,20 +37,19 @@ def build_tags(%{user: user}) do
with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
[ [
title_tag(user), title_tag(user),
{:meta, [property: "twitter:description", content: truncated_bio], []}, {:meta, [name: "twitter:description", content: truncated_bio], []},
image_tag(user), image_tag(user),
{:meta, [property: "twitter:card", content: "summary"], []} {:meta, [name: "twitter:card", content: "summary"], []}
] ]
end end
end end
defp title_tag(user) do 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 end
def image_tag(user) do 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 end
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do 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 case Utils.fetch_media_type(@media_types, url["mediaType"]) do
"audio" -> "audio" ->
[ [
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [name: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player:width", content: "480"], []}, {:meta, [name: "twitter:player:width", content: "480"], []},
{:meta, [property: "twitter:player:height", content: "80"], []}, {:meta, [name: "twitter:player:height", content: "80"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []} {:meta, [name: "twitter:player", content: player_url(id)], []}
| acc | acc
] ]
@ -74,10 +73,10 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
# workaround. # workaround.
"image" -> "image" ->
[ [
{:meta, [property: "twitter:card", content: "summary_large_image"], []}, {:meta, [name: "twitter:card", content: "summary_large_image"], []},
{:meta, {:meta,
[ [
property: "twitter:player", name: "twitter:player",
content: MediaProxy.url(url["href"]) content: MediaProxy.url(url["href"])
], []} ], []}
| acc | acc
@ -90,14 +89,14 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
width = url["width"] || 480 width = url["width"] || 480
[ [
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [name: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []}, {:meta, [name: "twitter:player", content: player_url(id)], []},
{:meta, [property: "twitter:player:width", content: "#{width}"], []}, {:meta, [name: "twitter:player:width", content: "#{width}"], []},
{:meta, [property: "twitter:player:height", content: "#{height}"], []}, {:meta, [name: "twitter:player:height", content: "#{height}"], []},
{:meta, [property: "twitter:player:stream", content: MediaProxy.url(url["href"])], {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])],
[]}, []},
{:meta, {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]],
[property: "twitter:player:stream:content_type", content: url["mediaType"]], []} []}
| acc | acc
] ]
@ -123,8 +122,8 @@ defp maybe_add_dimensions(metadata, url) do
!is_nil(url["height"]) && !is_nil(url["width"]) -> !is_nil(url["height"]) && !is_nil(url["width"]) ->
metadata ++ metadata ++
[ [
{:meta, [property: "twitter:player:width", content: "#{url["width"]}"], []}, {:meta, [name: "twitter:player:width", content: "#{url["width"]}"], []},
{:meta, [property: "twitter:player:height", content: "#{url["height"]}"], []} {:meta, [name: "twitter:player:height", content: "#{url["height"]}"], []}
] ]
true -> true ->

View file

@ -37,7 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
%{query_params: %{"name" => name}} = conn -> %{query_params: %{"name" => name}} = conn ->
name = escape_header_value(name) 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 ->
conn conn

View file

@ -25,6 +25,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"] @public_streams ["public", "public:local", "public:media", "public:local:media"]
@local_streams ["public:local", "public:local:media"]
@user_streams ["user", "user:notification", "direct"] @user_streams ["user", "user:notification", "direct"]
@doc "Expands and authorizes a stream, and registers the process for streaming." @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
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" @doc "Expand and authorizes a stream"
@spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} {:ok, topic :: String.t()} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{}) def get_topic(stream, user, oauth_token, params \\ %{})
# Allow all public steams. # Allow all public steams if the instance allows unauthenticated access.
def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do # 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} {:ok, stream}
else
{:error, :unauthorized}
end
end end
# Allow all hashtags streams. # Allow all hashtags streams.
@ -57,12 +81,20 @@ def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do
end end
# Allow remote instance streams. # Allow remote instance streams.
def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do 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} {:ok, "public:remote:" <> instance}
else
{:error, :unauthorized}
end
end end
def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do 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} {:ok, "public:remote:media:" <> instance}
else
{:error, :unauthorized}
end
end end
# Expand user streams. # Expand user streams.

View 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"]}

View file

@ -35,7 +35,7 @@ test "always add user and list topics" do
setup do setup do
activity = %Activity{ activity = %Activity{
object: %Object{data: %{"type" => "Note"}}, object: %Object{data: %{"type" => "Note"}},
data: %{"to" => [Pleroma.Constants.as_public()]} data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"}
} }
{:ok, activity: activity} {:ok, activity: activity}
@ -114,6 +114,55 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d
end end
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 describe "public visibility create events with attachments" do
setup do setup do
activity = %Activity{ activity = %Activity{
@ -152,9 +201,36 @@ test "non-local action produces public:remote:media topic", %{activity: activity
end end
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 describe "non-public visibility" do
test "produces direct topic" 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) topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "direct") assert Enum.member?(topics, "direct")

View file

@ -328,6 +328,32 @@ test "it disables notifications from strangers" do
refute Notification.create_notification(activity, followed) refute Notification.create_notification(activity, followed)
end 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 test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity) activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"]) 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 assert length(Notification.for_user(user)) == 1
end 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
end end

View file

@ -557,6 +557,21 @@ test "it fails gracefully with invalid email config" do
refute_email_sent() refute_email_sent()
end 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 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) clear_config([:instance, :account_activation_required], true)

View file

@ -732,9 +732,7 @@ test "it streams out the announce", %{announce: announce} do
]) do ]) do
{:ok, announce, _} = SideEffects.handle(announce) {:ok, announce, _} = SideEffects.handle(announce)
assert called( assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce)
)
assert called(Pleroma.Web.Push.send(:_)) assert called(Pleroma.Web.Push.send(:_))
end end

View file

@ -783,4 +783,42 @@ test "quote fetching should stop after n levels", _ do
} = Transmogrifier.fix_quote_url(note) } = Transmogrifier.fix_quote_url(note)
end end
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 end

View file

@ -225,6 +225,20 @@ test "superusers deleting non-local posts won't federate the delete" do
refute Activity.get_by_id(post.id) refute Activity.get_by_id(post.id)
end 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 end
test "favoriting race condition" do test "favoriting race condition" do

View file

@ -1895,6 +1895,39 @@ test "getting a list of blocks" do
assert [%{"id" => ^id2}] = result assert [%{"id" => ^id2}] = result
end 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 test "account lookup", %{conn: conn} do
%{nickname: acct} = insert(:user, %{nickname: "nickname"}) %{nickname: acct} = insert(:user, %{nickname: "nickname"})
%{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"}) %{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"})

View file

@ -674,7 +674,10 @@ test "option limit is enforced", %{conn: conn} do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{ |> post("/api/v1/statuses", %{
"status" => "desu~", "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) %{"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", %{ |> post("/api/v1/statuses", %{
"status" => "...", "status" => "...",
"poll" => %{ "poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], "options" => [String.duplicate(".", limit + 1), "lol"],
"expires_in" => 1 "expires_in" => 1
} }
}) })
@ -772,6 +775,32 @@ test "scheduled poll", %{conn: conn} do
assert object.data["type"] == "Question" assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3 assert length(object.data["oneOf"]) == 3
end 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 end
test "get a status" do 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(activity1.id)
refute Activity.get_by_id(activity2.id) refute Activity.get_by_id(activity2.id)
end 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 end
describe "reblogging" do describe "reblogging" do

View file

@ -11,11 +11,24 @@ test "it renders all links with rel='me' from user bio" do
bio = 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">) ~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}) == [ assert RelMe.build_tags(%{user: user}) == [
{:link, [rel: "me", href: "http://some3.com"], []}, {: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
end end

View file

@ -22,10 +22,10 @@ test "it renders twitter card for user info" do
res = TwitterCard.build_tags(%{user: user}) res = TwitterCard.build_tags(%{user: user})
assert res == [ assert res == [
{:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
{:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, {:meta, [name: "twitter:description", content: "born 19 March 1994"], []},
{:meta, [property: "twitter:image", content: avatar_url], []}, {:meta, [name: "twitter:image", content: avatar_url], []},
{:meta, [property: "twitter:card", content: "summary"], []} {:meta, [name: "twitter:card", content: "summary"], []}
] ]
end 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}) result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
assert [ 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", content: "pleroma in a nutshell"], []}, {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
{: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 ] == result
end 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}) result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
assert [ assert [
{:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
{:meta, {:meta,
[ [
property: "twitter:description", name: "twitter:description",
content: "Public service announcement on caffeine consumption" 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 ] == result
end 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}) result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
assert [ 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", content: "pleroma in a nutshell"], []}, {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
{: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 ] == result
end 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}) result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
assert [ 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", content: "pleroma in a nutshell"], []}, {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
{:meta, [property: "twitter:card", content: "summary_large_image"], []}, {:meta, [name: "twitter:card", content: "summary_large_image"], []},
{:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, {:meta, [name: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []},
{:meta, [property: "twitter:player:width", content: "1280"], []}, {:meta, [name: "twitter:player:width", content: "1280"], []},
{:meta, [property: "twitter:player:height", content: "1024"], []}, {:meta, [name: "twitter:player:height", content: "1024"], []},
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [name: "twitter:card", content: "player"], []},
{:meta, {:meta,
[ [
property: "twitter:player", name: "twitter:player",
content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id)
], []}, ], []},
{:meta, [property: "twitter:player:width", content: "800"], []}, {:meta, [name: "twitter:player:width", content: "800"], []},
{:meta, [property: "twitter:player:height", content: "600"], []}, {:meta, [name: "twitter:player:height", content: "600"], []},
{:meta, {:meta,
[ [
property: "twitter:player:stream", name: "twitter:player:stream",
content: "https://pleroma.gov/about/juche.webm" 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 ] == result
end end
end end

View file

@ -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", %{ test "sends Content-Disposition header when name param is set", %{
attachment_url: attachment_url attachment_url: attachment_url
} do } do
conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif])
assert Enum.any?( assert Enum.any?(
conn.resp_headers, conn.resp_headers,
&(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
) )
end end
@ -48,7 +48,7 @@ test "removes control characters from the Content-Disposition header", %{
assert Enum.any?( assert Enum.any?(
conn.resp_headers, conn.resp_headers,
&(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
) )
end end
end end

View file

@ -25,6 +25,26 @@ test "allows public" do
assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil)
end 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 test "allows instance streams" do
assert {:ok, "public:remote:lain.com"} = assert {:ok, "public:remote:lain.com"} =
Streamer.get_topic("public:remote", nil, nil, %{"instance" => "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
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)", %{ test "allows user streams (with proper OAuth token scopes)", %{
user: user, user: user,
token: read_oauth_token token: read_oauth_token

View file

@ -1085,6 +1085,14 @@ def get("http://404.site" <> _, _, _, _) do
}} }}
end end
def get("https://404.site" <> _, _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get( def get(
"https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c", "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 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 def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{