Merge remote-tracking branch 'origin/develop' into merge-ogp-twitter-parsers

This commit is contained in:
Egor Kislitsyn 2020-06-15 16:03:40 +04:00
commit 58e4e3db8b
No known key found for this signature in database
GPG key ID: 1B49CB15B71E7805
27 changed files with 371 additions and 182 deletions

View file

@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Filtering of push notifications on activities from blocked domains - Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger - Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP - `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
## [Unreleased (patch)] ## [Unreleased (patch)]

View file

@ -1623,14 +1623,12 @@
# %{ # %{
# group: :pleroma, # group: :pleroma,
# key: :mrf_user_allowlist, # key: :mrf_user_allowlist,
# type: :group, # type: :map,
# description: # description:
# "The keys in this section are the domain names that the policy should apply to." <> # "The keys in this section are the domain names that the policy should apply to." <>
# " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID", # " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
# children: [
# ["example.org": ["https://example.org/users/admin"]],
# suggestions: [ # suggestions: [
# ["example.org": ["https://example.org/users/admin"]] # %{"example.org" => ["https://example.org/users/admin"]}
# ] # ]
# ] # ]
# }, # },

View file

@ -138,8 +138,9 @@ their ActivityPub ID.
An example: An example:
```elixir ```elixir
config :pleroma, :mrf_user_allowlist, config :pleroma, :mrf_user_allowlist, %{
"example.org": ["https://example.org/users/admin"] "example.org" => ["https://example.org/users/admin"]
}
``` ```
#### :mrf_object_age #### :mrf_object_age

View file

@ -37,18 +37,17 @@ server {
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
ssl_session_timeout 5m; ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# Add TLSv1.0 to support older devices ssl_protocols TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.2;
# Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.)
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers off;
# In case of an old server with an OpenSSL version of 1.0.2 or below, # In case of an old server with an OpenSSL version of 1.0.2 or below,
# leave only prime256v1 or comment out the following line. # leave only prime256v1 or comment out the following line.
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;

View file

@ -31,6 +31,10 @@ defmodule Pleroma.Activity do
field(:recipients, {:array, :string}, default: []) field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true) field(:thread_muted?, :boolean, virtual: true)
# A field that can be used if you need to join some kind of other
# id to order / paginate this field by
field(:pagination_id, :string, virtual: true)
# This is a fake relation, # This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor # do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)

View file

@ -4,9 +4,10 @@
defmodule Pleroma.Config.DeprecationWarnings do defmodule Pleroma.Config.DeprecationWarnings do
require Logger require Logger
alias Pleroma.Config
def check_hellthread_threshold do def check_hellthread_threshold do
if Pleroma.Config.get([:mrf_hellthread, :threshold]) do if Config.get([:mrf_hellthread, :threshold]) do
Logger.warn(""" Logger.warn("""
!!!DEPRECATION WARNING!!! !!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md. You are using the old configuration mechanism for the hellthread filter. Please check config.md.
@ -14,7 +15,29 @@ def check_hellthread_threshold do
end end
end end
def mrf_user_allowlist do
config = Config.get(:mrf_user_allowlist)
if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do
rewritten =
Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc ->
Map.put(acc, to_string(k), v)
end)
Config.put(:mrf_user_allowlist, rewritten)
Logger.error("""
!!!DEPRECATION WARNING!!!
As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format.
Pleroma 2.1 will remove support for the old format. Please change your configuration to match this:
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
""")
end
end
def warn do def warn do
check_hellthread_threshold() check_hellthread_threshold()
mrf_user_allowlist()
end end
end end

View file

@ -166,8 +166,16 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility})
query query
|> join(:left, [n, a], mutated_activity in Pleroma.Activity, |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on: on:
fragment("?->>'context'", a.data) == fragment(
fragment("?->>'context'", mutated_activity.data) and "COALESCE((?->'object')->>'id', ?->>'object')",
a.data,
a.data
) ==
fragment(
"COALESCE((?->'object')->>'id', ?->>'object')",
mutated_activity.data,
mutated_activity.data
) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
fragment("?->>'type'", mutated_activity.data) == "Create", fragment("?->>'type'", mutated_activity.data) == "Create",
as: :mutated_activity as: :mutated_activity
@ -541,6 +549,7 @@ def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
def skip?(%Activity{} = activity, %User{} = user) do def skip?(%Activity{} = activity, %User{} = user) do
[ [
:self, :self,
:invisible,
:followers, :followers,
:follows, :follows,
:non_followers, :non_followers,
@ -557,6 +566,12 @@ def skip?(:self, %Activity{} = activity, %User{} = user) do
activity.data["actor"] == user.ap_id activity.data["actor"] == user.ap_id
end end
def skip?(:invisible, %Activity{} = activity, _) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
User.invisible?(user)
end
def skip?( def skip?(
:followers, :followers,
%Activity{} = activity, %Activity{} = activity,

View file

@ -1488,6 +1488,7 @@ def perform(:delete, %User{} = user) do
end) end)
delete_user_activities(user) delete_user_activities(user)
delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user) delete_outgoing_pending_follow_requests(user)
@ -1576,6 +1577,13 @@ def follow_import(%User{} = follower, followed_identifiers)
}) })
end end
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|> Repo.delete_all()
end
def delete_user_activities(%User{ap_id: ap_id} = user) do def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id ap_id
|> Activity.Queries.by_actor() |> Activity.Queries.by_actor()

View file

@ -32,25 +32,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil -> true
user -> User.following?(user, actor)
end
end)
{recipients, to, cc}
end
defp get_recipients(%{"type" => "Create"} = data) do defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", []) to = Map.get(data, "to", [])
cc = Map.get(data, "cc", []) cc = Map.get(data, "cc", [])
@ -721,6 +702,26 @@ defp user_activities_recipients(%{reading_user: reading_user}) do
end end
end end
defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'type' != ? or ?->>'actor' != ?",
activity.data,
"Announce",
object.data,
^actor
)
)
end
defp restrict_announce_object_actor(query, _), do: query
defp restrict_since(query, %{since_id: ""}), do: query defp restrict_since(query, %{since_id: ""}), do: query
defp restrict_since(query, %{since_id: since_id}) do defp restrict_since(query, %{since_id: since_id}) do
@ -1144,6 +1145,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts) |> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_chat_messages(opts) |> exclude_chat_messages(opts)
@ -1170,12 +1172,11 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
|> Activity.Queries.by_type("Like") |> Activity.Queries.by_type("Like")
|> Activity.with_joined_object() |> Activity.with_joined_object()
|> Object.with_joined_activity() |> Object.with_joined_activity()
|> select([_like, object, activity], %{activity | object: object}) |> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
|> order_by([like, _, _], desc_nulls_last: like.id) |> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated( |> Pagination.fetch_paginated(
Map.merge(params, %{skip_order: true}), Map.merge(params, %{skip_order: true}),
pagination, pagination
:object_activity
) )
end end

View file

@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do
allow_list = allow_list =
Config.get( Config.get(
[:mrf_user_allowlist, String.to_atom(actor_info.host)], [:mrf_user_allowlist, actor_info.host],
[] []
) )

View file

@ -333,7 +333,8 @@ def favourites_operation do
%Operation{ %Operation{
tags: ["Statuses"], tags: ["Statuses"],
summary: "Favourited statuses", summary: "Favourited statuses",
description: "Statuses the user has favourited", description:
"Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
operationId: "StatusController.favourites", operationId: "StatusController.favourites",
parameters: pagination_params(), parameters: pagination_params(),
security: [%{"oAuth" => ["read:favourites"]}], security: [%{"oAuth" => ["read:favourites"]}],

View file

@ -57,35 +57,36 @@ def add_link_headers(conn, activities, extra_params) do
end end
end end
@id_keys Pagination.page_keys() -- ["limit", "order"]
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
params =
conn.params
|> Map.drop(Map.keys(conn.path_params))
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
%{
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
"prev" => current_url(conn, Map.put(params, :min_id, min_id)),
"id" => current_url(conn)
}
end
def get_pagination_fields(conn, activities, extra_params \\ %{}) do def get_pagination_fields(conn, activities, extra_params \\ %{}) do
case List.last(activities) do case List.last(activities) do
%{id: max_id} -> %{pagination_id: max_id} when not is_nil(max_id) ->
params = %{pagination_id: min_id} =
conn.params
|> Map.drop(Map.keys(conn.path_params))
|> Map.merge(extra_params)
|> Map.drop(Pagination.page_keys() -- ["limit", "order"])
min_id =
activities activities
|> List.first() |> List.first()
|> Map.get(:id)
fields = %{ build_pagination_fields(conn, min_id, max_id, extra_params)
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
"prev" => current_url(conn, Map.put(params, :min_id, min_id))
}
# Generating an `id` without already present pagination keys would %{id: max_id} ->
# need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` %{id: min_id} =
# instead of the `q.id > ^min_id` and `q.id < ^max_id`. activities
# This is because we only have ids present inside of the page, while |> List.first()
# `min_id`, `since_id` and `max_id` requires to know one outside of it.
if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do build_pagination_fields(conn, min_id, max_id, extra_params)
Map.put(fields, "id", current_url(conn, conn.params))
else
fields
end
_ -> _ ->
%{} %{}

View file

@ -48,6 +48,7 @@ def home(%{assigns: %{user: user}} = conn, params) do
|> Map.put(:blocking_user, user) |> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user) |> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user) |> Map.put(:reply_filtering_user, user)
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user) |> Map.put(:user, user)
activities = activities =

View file

@ -46,6 +46,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
activities activities
|> Enum.filter(&(&1.data["type"] == "Move")) |> Enum.filter(&(&1.data["type"] == "Move"))
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
|> Enum.filter(& &1)
actors = actors =
activities activities
@ -84,50 +85,45 @@ def render(
# Note: :relationships contain user mutes (needed for :muted flag in :status) # Note: :relationships contain user mutes (needed for :muted flag in :status)
status_render_opts = %{relationships: opts[:relationships]} status_render_opts = %{relationships: opts[:relationships]}
with %{id: _} = account <- account =
AccountView.render( AccountView.render(
"show.json", "show.json",
%{user: actor, for: reading_user} %{user: actor, for: reading_user}
) do )
response = %{
id: to_string(notification.id), response = %{
type: notification.type, id: to_string(notification.id),
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), type: notification.type,
account: account, created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
pleroma: %{ account: account,
is_seen: notification.seen pleroma: %{
} is_seen: notification.seen
} }
}
case notification.type do case notification.type do
"mention" -> "mention" ->
put_status(response, activity, reading_user, status_render_opts) put_status(response, activity, reading_user, status_render_opts)
"favourite" -> "favourite" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts) put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"reblog" -> "reblog" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts) put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" -> "move" ->
put_target(response, activity, reading_user, %{}) put_target(response, activity, reading_user, %{})
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
response response
|> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity) |> put_emoji(activity)
"pleroma:chat_mention" -> "pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts) put_chat_message(response, activity, reading_user, status_render_opts)
type when type in ["follow", "follow_request"] -> type when type in ["follow", "follow_request"] ->
response response
_ ->
nil
end
else
_ -> nil
end end
end end

View file

@ -377,8 +377,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url_data = URI.parse(page_url) page_url_data = URI.parse(page_url)
page_url_data = page_url_data =
if rich_media[:url] != nil do if is_binary(rich_media["url"]) do
URI.merge(page_url_data, URI.parse(rich_media[:url])) URI.merge(page_url_data, URI.parse(rich_media["url"]))
else else
page_url_data page_url_data
end end
@ -386,11 +386,9 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url = page_url_data |> to_string page_url = page_url_data |> to_string
image_url = image_url =
if rich_media[:image] != nil do if is_binary(rich_media["image"]) do
URI.merge(page_url_data, URI.parse(rich_media[:image])) URI.merge(page_url_data, URI.parse(rich_media["image"]))
|> to_string |> to_string
else
nil
end end
%{ %{
@ -399,8 +397,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
provider_url: page_url_data.scheme <> "://" <> page_url_data.host, provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url, url: page_url,
image: image_url |> MediaProxy.url(), image: image_url |> MediaProxy.url(),
title: rich_media[:title] || "", title: rich_media["title"] || "",
description: rich_media[:description] || "", description: rich_media["description"] || "",
pleroma: %{ pleroma: %{
opengraph: rich_media opengraph: rich_media
} }

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.RichMedia.Parser alias Pleroma.Web.RichMedia.Parser
@spec validate_page_url(any()) :: :ok | :error @spec validate_page_url(URI.t() | binary()) :: :ok | :error
defp validate_page_url(page_url) when is_binary(page_url) do defp validate_page_url(page_url) when is_binary(page_url) do
validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
@ -18,8 +18,8 @@ defp validate_page_url(page_url) when is_binary(page_url) do
|> parse_uri(page_url) |> parse_uri(page_url)
end end
defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) defp validate_page_url(%URI{host: host, scheme: "https", authority: authority})
when scheme == "https" and not is_nil(authority) do when is_binary(authority) do
cond do cond do
host in Config.get([:rich_media, :ignore_hosts], []) -> host in Config.get([:rich_media, :ignore_hosts], []) ->
:error :error

View file

@ -91,7 +91,7 @@ defp parse_url(url) do
html html
|> parse_html() |> parse_html()
|> maybe_parse() |> maybe_parse()
|> Map.put(:url, url) |> Map.put("url", url)
|> clean_parsed_data() |> clean_parsed_data()
|> check_parsed_data() |> check_parsed_data()
rescue rescue
@ -111,8 +111,8 @@ defp maybe_parse(html) do
end) end)
end end
defp check_parsed_data(%{title: title} = data) defp check_parsed_data(%{"title" => title} = data)
when is_binary(title) and byte_size(title) > 0 do when is_binary(title) and title != "" do
{:ok, data} {:ok, data}
end end
@ -123,11 +123,7 @@ defp check_parsed_data(data) do
defp clean_parsed_data(data) do defp clean_parsed_data(data) do
data data
|> Enum.reject(fn {key, val} -> |> Enum.reject(fn {key, val} ->
with {:ok, _} <- Jason.encode(%{key => val}) do not match?({:ok, _}, Jason.encode(%{key => val}))
false
else
_ -> true
end
end) end)
|> Map.new() |> Map.new()
end end

View file

@ -22,19 +22,19 @@ defp normalize_attributes(html_node, prefix, key_name, value_name) do
{_tag, attributes, _children} = html_node {_tag, attributes, _children} = html_node
data = data =
Enum.into(attributes, %{}, fn {name, value} -> Map.new(attributes, fn {name, value} ->
{name, String.trim_leading(value, "#{prefix}:")} {name, String.trim_leading(value, "#{prefix}:")}
end) end)
%{String.to_atom(data[key_name]) => data[value_name]} %{data[key_name] => data[value_name]}
end end
defp maybe_put_title(%{title: _} = meta, _), do: meta defp maybe_put_title(%{"title" => _} = meta, _), do: meta
defp maybe_put_title(meta, html) when meta != %{} do defp maybe_put_title(meta, html) when meta != %{} do
case get_page_title(html) do case get_page_title(html) do
"" -> meta "" -> meta
title -> Map.put_new(meta, :title, title) title -> Map.put_new(meta, "title", title)
end end
end end

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
def parse(html, _data) do def parse(html, _data) do
with elements = [_ | _] <- get_discovery_data(html), with elements = [_ | _] <- get_discovery_data(html),
{:ok, oembed_url} <- get_oembed_url(elements), oembed_url when is_binary(oembed_url) <- get_oembed_url(elements),
{:ok, oembed_data} <- get_oembed_data(oembed_url) do {:ok, oembed_data} <- get_oembed_data(oembed_url) do
oembed_data oembed_data
else else
@ -17,19 +17,13 @@ defp get_discovery_data(html) do
html |> Floki.find("link[type='application/json+oembed']") html |> Floki.find("link[type='application/json+oembed']")
end end
defp get_oembed_url(nodes) do defp get_oembed_url([{"link", attributes, _children} | _]) do
{"link", attributes, _children} = nodes |> hd() Enum.find_value(attributes, fn {k, v} -> if k == "href", do: v end)
{:ok, Enum.into(attributes, %{})["href"]}
end end
defp get_oembed_data(url) do defp get_oembed_data(url) do
{:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media]) with {:ok, %Tesla.Env{body: json}} <- Pleroma.HTTP.get(url, [], adapter: [pool: :media]) do
Jason.decode(json)
{:ok, data} = Jason.decode(json) end
data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
{:ok, data}
end end
end end

View file

@ -0,0 +1,18 @@
defmodule Pleroma.Repo.Migrations.DeleteNotificationsFromInvisibleUsers do
use Ecto.Migration
import Ecto.Query
alias Pleroma.Repo
def up do
Pleroma.Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where(
[n, a],
fragment("? in (SELECT ap_id FROM users WHERE invisible = true)", a.actor)
)
|> Repo.delete_all()
end
def down, do: :ok
end

View file

@ -306,6 +306,14 @@ test "it doesn't create subscription notifications if the recipient cannot see t
assert {:ok, []} == Notification.create_notifications(status) assert {:ok, []} == Notification.create_notifications(status)
end end
test "it disables notifications from people who are invisible" do
author = insert(:user, invisible: true)
user = insert(:user)
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
refute Notification.create_notification(status, user)
end
end end
describe "follow / follow_request notifications" do describe "follow / follow_request notifications" do

View file

@ -574,7 +574,7 @@ test "doesn't return transitive interactions concerning blocked users" do
refute Enum.member?(activities, activity_four) refute Enum.member?(activities, activity_four)
end end
test "doesn't return announce activities concerning blocked users" do test "doesn't return announce activities with blocked users in 'to'" do
blocker = insert(:user) blocker = insert(:user)
blockee = insert(:user) blockee = insert(:user)
friend = insert(:user) friend = insert(:user)
@ -596,6 +596,39 @@ test "doesn't return announce activities concerning blocked users" do
refute Enum.member?(activities, activity_three.id) refute Enum.member?(activities, activity_three.id)
end end
test "doesn't return announce activities with blocked users in 'cc'" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
assert object = Pleroma.Object.normalize(activity_two)
data = %{
"actor" => friend.ap_id,
"object" => object.data["id"],
"context" => object.data["context"],
"type" => "Announce",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [blockee.ap_id]
}
assert {:ok, activity_three} = ActivityPub.insert(data)
activities =
ActivityPub.fetch_activities([], %{blocking_user: blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return activities from blocked domains" do test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone" domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
@ -1643,6 +1676,40 @@ test "home timeline with reply_visibility `self`", %{
assert Enum.all?(visible_ids, &(&1 in activities_ids)) assert Enum.all?(visible_ids, &(&1 in activities_ids))
end end
test "filtering out announces where the user is the actor of the announced message" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
User.follow(user, other_user)
{:ok, post} = CommonAPI.post(user, %{status: "yo"})
{:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"})
{:ok, _announce} = CommonAPI.repeat(post.id, other_user)
{:ok, _announce} = CommonAPI.repeat(post.id, third_user)
{:ok, announce} = CommonAPI.repeat(other_post.id, other_user)
params = %{
type: ["Announce"]
}
results =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
assert length(results) == 3
params = %{
type: ["Announce"],
announce_filtering_user: user
}
[result] =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
assert result.id == announce.id
end
end end
describe "replies filtering with private messages" do describe "replies filtering with private messages" do

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
setup do: clear_config([:mrf_user_allowlist, :localhost]) setup do: clear_config(:mrf_user_allowlist)
test "pass filter if allow list is empty" do test "pass filter if allow list is empty" do
actor = insert(:user) actor = insert(:user)
@ -17,14 +17,14 @@ test "pass filter if allow list is empty" do
test "pass filter if allow list isn't empty and user in allow list" do test "pass filter if allow list isn't empty and user in allow list" do
actor = insert(:user) actor = insert(:user)
Pleroma.Config.put([:mrf_user_allowlist, :localhost], [actor.ap_id, "test-ap-id"]) Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]})
message = %{"actor" => actor.ap_id} message = %{"actor" => actor.ap_id}
assert UserAllowListPolicy.filter(message) == {:ok, message} assert UserAllowListPolicy.filter(message) == {:ok, message}
end end
test "rejected if allow list isn't empty and user not in allow list" do test "rejected if allow list isn't empty and user not in allow list" do
actor = insert(:user) actor = insert(:user)
Pleroma.Config.put([:mrf_user_allowlist, :localhost], ["test-ap-id"]) Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]})
message = %{"actor" => actor.ap_id} message = %{"actor" => actor.ap_id}
assert UserAllowListPolicy.filter(message) == {:reject, nil} assert UserAllowListPolicy.filter(message) == {:reject, nil}
end end

View file

@ -313,6 +313,33 @@ test "filters notifications for Announce activities" do
assert public_activity.id in activity_ids assert public_activity.id in activity_ids
refute unlisted_activity.id in activity_ids refute unlisted_activity.id in activity_ids
end end
test "doesn't return less than the requested amount of records when the user's reply is liked" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
{:ok, mention} =
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
{:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:ok, reply} =
CommonAPI.post(other_user, %{
status: ".",
visibility: "public",
in_reply_to_status_id: activity.id
})
{:ok, _favorite} = CommonAPI.favorite(user, reply.id)
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert [reply.id, mention.id] == activity_ids
end
end end
test "filters notifications using exclude_types" do test "filters notifications using exclude_types" do

View file

@ -1541,14 +1541,49 @@ test "context" do
} = response } = response
end end
test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
{:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
{:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
result =
conn
|> get("/api/v1/favourites?limit=1")
assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
assert post_id == second_post.id
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
[_, max_id] = Regex.run(~r/max_id=(.*)>;/, next)
assert max_id == third_favorite.id
result =
conn
|> get("/api/v1/favourites?max_id=#{max_id}")
assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
json_response_and_validate_schema(result, 200)
assert first_post_id == first_post.id
assert third_post_id == third_post.id
end
test "returns the favorites of a user" do test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"]) %{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user) other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{status: "traps are happy"}) {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
{:ok, _} = CommonAPI.favorite(user, activity.id) {:ok, last_like} = CommonAPI.favorite(user, activity.id)
first_conn = get(conn, "/api/v1/favourites") first_conn = get(conn, "/api/v1/favourites")
@ -1566,9 +1601,7 @@ test "returns the favorites of a user" do
{:ok, _} = CommonAPI.favorite(user, second_activity.id) {:ok, _} = CommonAPI.favorite(user, second_activity.id)
last_like = status["id"] second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response_and_validate_schema(second_conn, 200) assert [second_status] = json_response_and_validate_schema(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id) assert second_status["id"] == to_string(second_activity.id)

View file

@ -139,9 +139,7 @@ test "Follow notification" do
test_notifications_rendering([notification], followed, [expected]) test_notifications_rendering([notification], followed, [expected])
User.perform(:delete, follower) User.perform(:delete, follower)
notification = Notification |> Repo.one() |> Repo.preload(:activity) refute Repo.one(Notification)
test_notifications_rendering([notification], followed, [])
end end
@tag capture_log: true @tag capture_log: true

View file

@ -60,19 +60,19 @@ test "returns error when no metadata present" do
test "doesn't just add a title" do test "doesn't just add a title" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/non-ogp") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/non-ogp") ==
{:error, {:error,
"Found metadata was invalid or incomplete: %{url: \"http://example.com/non-ogp\"}"} "Found metadata was invalid or incomplete: %{\"url\" => \"http://example.com/non-ogp\"}"}
end end
test "parses ogp" do test "parses ogp" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") ==
{:ok, {:ok,
%{ %{
image: "http://ia.media-imdb.com/images/rock.jpg", "image" => "http://ia.media-imdb.com/images/rock.jpg",
title: "The Rock", "title" => "The Rock",
description: "description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
type: "video.movie", "type" => "video.movie",
url: "http://example.com/ogp" "url" => "http://example.com/ogp"
}} }}
end end
@ -80,12 +80,12 @@ test "falls back to <title> when ogp:title is missing" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp-missing-title") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp-missing-title") ==
{:ok, {:ok,
%{ %{
image: "http://ia.media-imdb.com/images/rock.jpg", "image" => "http://ia.media-imdb.com/images/rock.jpg",
title: "The Rock (1996)", "title" => "The Rock (1996)",
description: "description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
type: "video.movie", "type" => "video.movie",
url: "http://example.com/ogp-missing-title" "url" => "http://example.com/ogp-missing-title"
}} }}
end end
@ -93,12 +93,12 @@ test "parses twitter card" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") ==
{:ok, {:ok,
%{ %{
card: "summary", "card" => "summary",
site: "@flickr", "site" => "@flickr",
image: "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
title: "Small Island Developing States Photo Submission", "title" => "Small Island Developing States Photo Submission",
description: "View the album on Flickr.", "description" => "View the album on Flickr.",
url: "http://example.com/twitter-card" "url" => "http://example.com/twitter-card"
}} }}
end end
@ -106,27 +106,28 @@ test "parses OEmbed" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") ==
{:ok, {:ok,
%{ %{
author_name: "bees", "author_name" => "bees",
author_url: "https://www.flickr.com/photos/bees/", "author_url" => "https://www.flickr.com/photos/bees/",
cache_age: 3600, "cache_age" => 3600,
flickr_type: "photo", "flickr_type" => "photo",
height: "768", "height" => "768",
html: "html" =>
"<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>",
license: "All Rights Reserved", "license" => "All Rights Reserved",
license_id: 0, "license_id" => 0,
provider_name: "Flickr", "provider_name" => "Flickr",
provider_url: "https://www.flickr.com/", "provider_url" => "https://www.flickr.com/",
thumbnail_height: 150, "thumbnail_height" => 150,
thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", "thumbnail_url" =>
thumbnail_width: 150, "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
title: "Bacon Lollys", "thumbnail_width" => 150,
type: "photo", "title" => "Bacon Lollys",
url: "http://example.com/oembed", "type" => "photo",
version: "1.0", "url" => "http://example.com/oembed",
web_page: "https://www.flickr.com/photos/bees/2362225867/", "version" => "1.0",
web_page_short_url: "https://flic.kr/p/4AK2sc", "web_page" => "https://www.flickr.com/photos/bees/2362225867/",
width: "1024" "web_page_short_url" => "https://flic.kr/p/4AK2sc",
"width" => "1024"
}} }}
end end