Merge branch 'develop' into issue/1855

This commit is contained in:
Maksim Pechnikov 2020-06-15 15:24:55 +03:00
commit 579763126f
42 changed files with 717 additions and 234 deletions

View file

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [unreleased] ## [unreleased]
### Changed ### Changed
- MFR policy to set global expiration for all local Create activities
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- **Breaking:** Emoji API: changed methods and renamed routes. - **Breaking:** Emoji API: changed methods and renamed routes.
@ -26,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit). - Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required. - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App. - Mix task to create trusted OAuth App.
- Mix task to reset MFA for user accounts
- Notifications: Added `follow_request` notification type. - Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy - Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
@ -37,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add support for filtering replies in public and home timelines - Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view. - Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs
</details> </details>
### Fixed ### Fixed
@ -46,6 +49,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

@ -371,6 +371,8 @@
config :pleroma, :mrf_subchain, match_actor: %{} config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365
config :pleroma, :mrf_vocabulary, config :pleroma, :mrf_vocabulary,
accept: [], accept: [],
reject: [] reject: []

View file

@ -1471,6 +1471,21 @@
} }
] ]
}, },
%{
group: :pleroma,
key: :mrf_activity_expiration,
label: "MRF Activity Expiration Policy",
type: :group,
description: "Adds expiration to all local Create Note activities",
children: [
%{
key: :days,
type: :integer,
description: "Default global expiration time for all local Create activities (in days)",
suggestions: [90, 365]
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_subchain, key: :mrf_subchain,
@ -1608,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

@ -44,3 +44,11 @@ Currently, only .zip archives are recognized as remote pack files and packs are
The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted). The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).
## Reload emoji packs
```sh tab="OTP"
./bin/pleroma_ctl emoji reload
```
This command only works with OTP releases.

View file

@ -135,6 +135,16 @@ mix pleroma.user reset_password <nickname>
``` ```
## Disable Multi Factor Authentication (MFA/2FA) for a user
```sh tab="OTP"
./bin/pleroma_ctl user reset_mfa <nickname>
```
```sh tab="From Source"
mix pleroma.user reset_mfa <nickname>
```
## Set the value of the given user's settings ## Set the value of the given user's settings
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user set <nickname> [option ...] ./bin/pleroma_ctl user set <nickname> [option ...]

View file

@ -39,7 +39,7 @@ To add configuration to your config file, you can copy it from the base config.
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default). * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default).
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt makes sense to use in production. * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt makes sense to use in production.
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)).
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
@ -49,7 +49,8 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)).
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@ -137,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
@ -154,6 +156,10 @@ config :pleroma, :mrf_user_allowlist,
* `rejected_shortcodes`: Regex-list of shortcodes to reject * `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk * `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
#### :mrf_activity_expiration
* `days`: Default global expiration time for all local Create activities (in days)
### :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

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

@ -237,6 +237,12 @@ def run(["gen-pack" | args]) do
end end
end end
def run(["reload"]) do
start_pleroma()
Pleroma.Emoji.reload()
IO.puts("Emoji packs have been reloaded.")
end
defp fetch_and_decode(from) do defp fetch_and_decode(from) do
with {:ok, json} <- fetch(from) do with {:ok, json} <- fetch(from) do
Jason.decode!(json) Jason.decode!(json)

View file

@ -144,6 +144,18 @@ def run(["reset_password", nickname]) do
end end
end end
def run(["reset_mfa", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, _token} <- Pleroma.MFA.disable(user) do
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["deactivate", nickname]) do def run(["deactivate", nickname]) do
start_pleroma() start_pleroma()

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

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics alias Pleroma.Activity.Ir.Topics
alias Pleroma.ActivityExpiration
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Constants alias Pleroma.Constants
alias Pleroma.Conversation alias Pleroma.Conversation
@ -31,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", [])
@ -146,12 +128,14 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
{:containment, :ok} <- {:containment, Containment.contain_child(map)}, {:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do {:ok, map, object} <- insert_full_object(map) do
{:ok, activity} = {:ok, activity} =
Repo.insert(%Activity{ %Activity{
data: map, data: map,
local: local, local: local,
actor: map["actor"], actor: map["actor"],
recipients: recipients recipients: recipients
}) }
|> Repo.insert()
|> maybe_create_activity_expiration()
# Splice in the child object if we have one. # Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object) activity = Maps.put_if_present(activity, :object, object)
@ -189,6 +173,14 @@ def notify_and_stream(activity) do
stream_out_participations(participations) stream_out_participations(participations)
end end
defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(result), do: result
defp create_or_bump_conversation(activity, actor) do defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity), with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor) do %User{} = user <- User.get_cached_by_ap_id(actor) do
@ -710,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
@ -1133,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)
@ -1159,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

@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(policies, %{} = object) do def filter(policies, %{} = object) do
policies policies
|> Enum.reduce({:ok, object}, fn |> Enum.reduce({:ok, object}, fn
policy, {:ok, object} -> policy, {:ok, object} -> policy.filter(object)
policy.filter(object) _, error -> error
_, error ->
error
end) end)
end end

View file

@ -0,0 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
@moduledoc "Adds expiration to all local Create activities"
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true
def filter(activity) do
activity =
if note?(activity) and local?(activity) do
maybe_add_expiration(activity)
else
activity
end
{:ok, activity}
end
@impl true
def describe, do: {:ok, %{}}
defp local?(%{"id" => id}) do
String.starts_with?(id, Pleroma.Web.Endpoint.url())
end
defp note?(activity) do
match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity)
end
defp maybe_add_expiration(activity) do
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
with %{"expires_at" => existing_expires_at} <- activity,
:lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
activity
else
_ -> Map.put(activity, "expires_at", expires_at)
end
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

@ -197,6 +197,13 @@ defp preview?(draft) do
defp changes(draft) do defp changes(draft) do
direct? = draft.visibility == "direct" direct? = draft.visibility == "direct"
additional = %{"cc" => draft.cc, "directMessage" => direct?}
additional =
case draft.expires_at do
%NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
_ -> additional
end
changes = changes =
%{ %{
@ -204,7 +211,7 @@ defp changes(draft) do
actor: draft.user, actor: draft.user,
context: draft.context, context: draft.context,
object: draft.object, object: draft.object,
additional: %{"cc" => draft.cc, "directMessage" => direct?} additional: additional
} }
|> Utils.maybe_add_list_data(draft.user, draft.visibility) |> Utils.maybe_add_list_data(draft.user, draft.visibility)

View file

@ -423,20 +423,10 @@ def listen(user, data) do
def post(user, %{status: _} = data) do def post(user, %{status: _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes ActivityPub.create(draft.changes, draft.preview?)
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end end
end end
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(result, _), do: result
def pin(id, %{ap_id: user_ap_id} = user) do def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{ with %Activity{
actor: ^user_ap_id, actor: ^user_ap_id,

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

@ -152,6 +152,7 @@ defp prepare_tags(query, add_joined_tag \\ true) do
defp preprocess_uri_query(query) do defp preprocess_uri_query(query) do
if query =~ ~r/https?:\/\// do if query =~ ~r/https?:\/\// do
query query
|> String.trim_trailing("/")
|> URI.parse() |> URI.parse()
|> Map.get(:path) |> Map.get(:path)
|> String.split("/") |> String.split("/")

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

@ -29,19 +29,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
{:ok, oembed_data} {:ok, 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

@ -4,6 +4,7 @@
defmodule Mix.Tasks.Pleroma.UserTest do defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.MFA
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
@ -278,6 +279,35 @@ test "no user to reset password" do
end end
end end
describe "running reset_mfa" do
test "disables MFA" do
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
}
)
Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "Multi-Factor Authentication disabled for #{user.nickname}"
assert %{enabled: false, totp: false} ==
user.nickname
|> User.get_cached_by_nickname()
|> MFA.mfa_settings()
end
test "no user to reset MFA" do
Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running invite" do describe "running invite" do
test "invite token is generated" do test "invite token is generated" do
assert capture_io(fn -> assert capture_io(fn ->

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
@ -1986,4 +2053,20 @@ test "it just returns the input if the user has no following/follower addresses"
end) =~ "Follower/Following counter update for #{user.ap_id} failed" end) =~ "Follower/Following counter update for #{user.ap_id} failed"
end end
end end
describe "global activity expiration" do
setup do: clear_config([:instance, :rewrite_policy])
test "creates an activity expiration for local Create activities" do
Pleroma.Config.put(
[:instance, :rewrite_policy],
Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
)
{:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"})
assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all()
end
end
end end

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
use ExUnit.Case, async: true
alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
@id Pleroma.Web.Endpoint.url() <> "/activities/cofe"
test "adds `expires_at` property" do
assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"type" => "Create",
"object" => %{"type" => "Note"}
})
assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
end
test "keeps existing `expires_at` if it less than the config setting" do
expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1)
assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"type" => "Create",
"expires_at" => expires_at,
"object" => %{"type" => "Note"}
})
end
test "overwrites existing `expires_at` if it greater than the config setting" do
too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2)
assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"type" => "Create",
"expires_at" => too_distant_future,
"object" => %{"type" => "Note"}
})
assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
end
test "ignores remote activities" do
assert {:ok, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"type" => "Create",
"object" => %{"type" => "Note"}
})
refute Map.has_key?(activity, "expires_at")
end
test "ignores non-Create/Note activities" do
assert {:ok, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"type" => "Follow"
})
refute Map.has_key?(activity, "expires_at")
assert {:ok, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"type" => "Create",
"object" => %{"type" => "Cofe"}
})
refute Map.has_key?(activity, "expires_at")
end
end

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

@ -120,6 +120,35 @@ test "constructs hashtags from search query", %{conn: conn} do
assert results["hashtags"] == [ assert results["hashtags"] == [
%{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"}
] ]
results =
conn
|> get(
"/api/v2/search?#{
URI.encode_query(%{
q:
"https://www.washingtonpost.com/sports/2020/06/10/" <>
"nascar-ban-display-confederate-flag-all-events-properties/"
})
}"
)
|> json_response_and_validate_schema(200)
assert results["hashtags"] == [
%{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"},
%{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"},
%{"name" => "display", "url" => "#{Web.base_url()}/tag/display"},
%{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"},
%{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"},
%{"name" => "all", "url" => "#{Web.base_url()}/tag/all"},
%{"name" => "events", "url" => "#{Web.base_url()}/tag/events"},
%{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"},
%{
"name" => "NascarBanDisplayConfederateFlagAllEventsProperties",
"url" =>
"#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties"
}
]
end end
test "excludes a blocked users from search results", %{conn: conn} do test "excludes a blocked users from search results", %{conn: conn} 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

View file

@ -19,11 +19,11 @@ test "parses twitter card with only name attributes" do
assert TwitterCard.parse(html, %{}) == assert TwitterCard.parse(html, %{}) ==
{:ok, {:ok,
%{ %{
"app:id:googleplay": "com.nytimes.android", "app:id:googleplay" => "com.nytimes.android",
"app:name:googleplay": "NYTimes", "app:name:googleplay" => "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622", "app:url:googleplay" => "nytimes://reader/id/100000006583622",
site: nil, "site" => nil,
title: "title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times" "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times"
}} }}
end end
@ -36,15 +36,15 @@ test "parses twitter card with only property attributes" do
assert TwitterCard.parse(html, %{}) == assert TwitterCard.parse(html, %{}) ==
{:ok, {:ok,
%{ %{
card: "summary_large_image", "card" => "summary_large_image",
description: "description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
image: "image" =>
"https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
"image:alt": "", "image:alt" => "",
title: "title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
url: "url" =>
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}} }}
end end
@ -57,19 +57,19 @@ test "parses twitter card with name & property attributes" do
assert TwitterCard.parse(html, %{}) == assert TwitterCard.parse(html, %{}) ==
{:ok, {:ok,
%{ %{
"app:id:googleplay": "com.nytimes.android", "app:id:googleplay" => "com.nytimes.android",
"app:name:googleplay": "NYTimes", "app:name:googleplay" => "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622", "app:url:googleplay" => "nytimes://reader/id/100000006583622",
card: "summary_large_image", "card" => "summary_large_image",
description: "description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
image: "image" =>
"https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
"image:alt": "", "image:alt" => "",
site: nil, "site" => nil,
title: "title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
url: "url" =>
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}} }}
end end
@ -86,11 +86,11 @@ test "respect only first title tag on the page" do
assert TwitterCard.parse(html, %{}) == assert TwitterCard.parse(html, %{}) ==
{:ok, {:ok,
%{ %{
site: "@atlasobscura", "site" => "@atlasobscura",
title: "title" =>
"The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura", "The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura",
card: "summary_large_image", "card" => "summary_large_image",
image: image_path "image" => image_path
}} }}
end end
@ -102,12 +102,12 @@ test "takes first founded title in html head if there is html markup error" do
assert TwitterCard.parse(html, %{}) == assert TwitterCard.parse(html, %{}) ==
{:ok, {:ok,
%{ %{
site: nil, "site" => nil,
title: "title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times", "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times",
"app:id:googleplay": "com.nytimes.android", "app:id:googleplay" => "com.nytimes.android",
"app:name:googleplay": "NYTimes", "app:name:googleplay" => "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622" "app:url:googleplay" => "nytimes://reader/id/100000006583622"
}} }}
end end
end end

View file

@ -11,7 +11,10 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
import Pleroma.Factory import Pleroma.Factory
import ExUnit.CaptureLog import ExUnit.CaptureLog
setup do: clear_config([ActivityExpiration, :enabled]) setup do
clear_config([ActivityExpiration, :enabled])
clear_config([:instance, :rewrite_policy])
end
test "deletes an expiration activity" do test "deletes an expiration activity" do
Pleroma.Config.put([ActivityExpiration, :enabled], true) Pleroma.Config.put([ActivityExpiration, :enabled], true)
@ -36,6 +39,35 @@ test "deletes an expiration activity" do
refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
end end
test "works with ActivityExpirationPolicy" do
Pleroma.Config.put([ActivityExpiration, :enabled], true)
Pleroma.Config.put(
[:instance, :rewrite_policy],
Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
)
user = insert(:user)
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
{:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"})
past_date =
NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second)
activity
|> Repo.preload(:expiration)
|> Map.get(:expiration)
|> Ecto.Changeset.change(%{scheduled_at: past_date})
|> Repo.update!()
Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] =
Pleroma.Repo.all(Pleroma.Activity)
end
describe "delete_activity/1" do describe "delete_activity/1" do
test "adds log message if activity isn't find" do test "adds log message if activity isn't find" do
assert capture_log([level: :error], fn -> assert capture_log([level: :error], fn ->