Merge branch 'issue/941' into 'develop'

[#941] add option skip_thread_containment

See merge request pleroma/pleroma!1237
This commit is contained in:
lain 2019-06-04 14:58:13 +00:00
commit f178a6f3f0
17 changed files with 249 additions and 40 deletions

View file

@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MRF: Support for rejecting reports from specific instances (`mrf_simple`) - MRF: Support for rejecting reports from specific instances (`mrf_simple`)
- MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`) - MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
- MRF: Support for running subchains. - MRF: Support for running subchains.
- Configuration: `skip_thread_containment` option
### Changed ### Changed
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer

View file

@ -243,7 +243,8 @@ config :pleroma, :instance,
max_report_comment_size: 1000, max_report_comment_size: 1000,
safe_dm_mentions: false, safe_dm_mentions: false,
healthcheck: false, healthcheck: false,
remote_post_retention_days: 90 remote_post_retention_days: 90,
skip_thread_containment: false
config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800 config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800

View file

@ -82,6 +82,7 @@ Additional parameters can be added to the JSON body/Form data:
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API - `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity - `default_scope` - the scope returned under `privacy` key in Source subentity
- `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads
### Pleroma Settings Store ### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.

View file

@ -111,6 +111,7 @@ config :pleroma, Pleroma.Emails.Mailer,
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
* `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``. * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.
* `remote_post_retention_days`: the default amount of days to retain remote posts when pruning the database * `remote_post_retention_days`: the default amount of days to retain remote posts when pruning the database
* `skip_thread_containment`: Skip filter out broken threads. the default is `false`.
## :app_account_creation ## :app_account_creation
REST API for creating an account settings REST API for creating an account settings

View file

@ -55,6 +55,8 @@ defmodule Pleroma.User.Info do
} }
) )
field(:skip_thread_containment, :boolean, default: false)
# Found in the wild # Found in the wild
# ap_id -> Where is this used? # ap_id -> Where is this used?
# bio -> Where is this used? # bio -> Where is this used?
@ -220,6 +222,7 @@ defmodule Pleroma.User.Info do
:hide_favorites, :hide_favorites,
:background, :background,
:show_role, :show_role,
:skip_thread_containment,
:pleroma_settings_store :pleroma_settings_store
]) ])
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -73,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit]) limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit String.length(content) <= limit
end end
@ -411,8 +412,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do
outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks]) outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Pleroma.Config.get([:activitypub, :unfollow_blocked]) unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked) follow_activity = fetch_latest_follow(blocker, blocked)
@ -557,14 +558,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, %{visibility: visibility}) defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do when visibility in @valid_visibilities do
query = from(
from( a in query,
a in query, where:
where: fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) )
)
query
end end
defp restrict_visibility(_query, %{visibility: visibility}) defp restrict_visibility(_query, %{visibility: visibility})
@ -574,17 +572,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, _visibility), do: query defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
query = do: query
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
query defp restrict_thread_visibility(
query,
%{"user" => %User{info: %{skip_thread_containment: true}}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end end
defp restrict_thread_visibility(query, _), do: query defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do def fetch_user_activities(user, reading_user, params \\ %{}) do
params = params =
@ -863,6 +868,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity) base_query = from(activity in Activity)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
base_query base_query
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts) |> maybe_preload_bookmarks(opts)
@ -882,7 +891,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_muted(opts) |> restrict_muted(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts) |> restrict_thread_visibility(opts, config)
|> restrict_replies(opts) |> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)

View file

@ -117,7 +117,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Enum.dedup() |> Enum.dedup()
info_params = info_params =
[:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] [
:no_rich_text,
:locked,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment
]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value -> add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)} {:ok, ControllerHelper.truthy_param?(value)}

View file

@ -124,7 +124,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
hide_followers: user.info.hide_followers, hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows, hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites, hide_favorites: user.info.hide_favorites,
relationship: relationship relationship: relationship,
skip_thread_containment: user.info.skip_thread_containment
} }
} }
|> maybe_put_role(user, opts[:for]) |> maybe_put_role(user, opts[:for])

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
use GenServer use GenServer
require Logger require Logger
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -224,11 +225,10 @@ defmodule Pleroma.Web.Streamer do
mutes = user.info.mutes || [] mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || [] reblog_mutes = user.info.muted_reblogs || []
parent = Object.normalize(item) with parent when not is_nil(parent) <- Object.normalize(item),
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or true <- thread_containment(item, user) do
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
send(socket.transport_pid, {:text, represent_update(item, user)}) send(socket.transport_pid, {:text, represent_update(item, user)})
end end
else else
@ -264,8 +264,8 @@ defmodule Pleroma.Web.Streamer do
blocks = user.info.blocks || [] blocks = user.info.blocks || []
mutes = user.info.mutes || [] mutes = user.info.mutes || []
unless item.actor in blocks or item.actor in mutes or with true <- Enum.all?([blocks, mutes], &(item.actor not in &1)),
not ActivityPub.contain_activity(item, user) do true <- thread_containment(item, user) do
send(socket.transport_pid, {:text, represent_update(item, user)}) send(socket.transport_pid, {:text, represent_update(item, user)})
end end
else else
@ -279,4 +279,15 @@ defmodule Pleroma.Web.Streamer do
end end
defp internal_topic(topic, _), do: topic defp internal_topic(topic, _), do: topic
@spec thread_containment(Activity.t(), User.t()) :: boolean()
defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true
defp thread_containment(activity, user) do
if Config.get([:instance, :skip_thread_containment]) do
true
else
ActivityPub.contain_activity(activity, user)
end
end
end end

View file

@ -632,7 +632,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp build_info_cng(user, params) do defp build_info_cng(user, params) do
info_params = info_params =
["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"] [
"no_rich_text",
"locked",
"hide_followers",
"hide_follows",
"hide_favorites",
"show_role",
"skip_thread_containment"
]
|> Enum.reduce(%{}, fn key, res -> |> Enum.reduce(%{}, fn key, res ->
if value = params[key] do if value = params[key] do
Map.put(res, key, value == "true") Map.put(res, key, value == "true")

View file

@ -118,7 +118,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"pleroma" => "pleroma" =>
%{ %{
"confirmation_pending" => user_info.confirmation_pending, "confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags "tags" => user.tags,
"skip_thread_containment" => user.info.skip_thread_containment
} }
|> maybe_with_activation_status(user, for_user) |> maybe_with_activation_status(user, for_user)
|> with_notification_settings(user, for_user) |> with_notification_settings(user, for_user)

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.ActivityPub.VisibilityTest do defmodule Pleroma.Web.ActivityPub.VisibilityTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -121,4 +122,46 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
test "get_visibility with directMessage flag" do test "get_visibility with directMessage flag" do
assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
end end
describe "entire_thread_visible_for_user?/2" do
test "returns false if not found activity", %{user: user} do
refute Visibility.entire_thread_visible_for_user?(%Activity{}, user)
end
test "returns true if activity hasn't 'Create' type", %{user: user} do
activity = insert(:like_activity)
assert Visibility.entire_thread_visible_for_user?(activity, user)
end
test "returns false when invalid recipients", %{user: user} do
author = insert(:user)
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["test-user"]}
)
)
refute Visibility.entire_thread_visible_for_user?(activity, user)
end
test "returns true if user following to author" do
author = insert(:user)
user = insert(:user, following: [author.ap_id])
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => [user.ap_id]}
)
)
assert Visibility.entire_thread_visible_for_user?(activity, user)
end
end
end end

View file

@ -67,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_favorites: true, hide_favorites: true,
hide_followers: false, hide_followers: false,
hide_follows: false, hide_follows: false,
relationship: %{} relationship: %{},
skip_thread_containment: false
} }
} }
@ -132,7 +133,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_favorites: true, hide_favorites: true,
hide_followers: false, hide_followers: false,
hide_follows: false, hide_follows: false,
relationship: %{} relationship: %{},
skip_thread_containment: false
} }
} }
@ -233,7 +235,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
domain_blocking: false, domain_blocking: false,
showing_reblogs: true, showing_reblogs: true,
endorsed: false endorsed: false
} },
skip_thread_containment: false
} }
} }

View file

@ -2539,6 +2539,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
assert user["pleroma"]["hide_followers"] == true assert user["pleroma"]["hide_followers"] == true
end end
test "updates the user's skip_thread_containment option", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
|> json_response(200)
assert response["pleroma"]["skip_thread_containment"] == true
assert refresh_record(user).info.skip_thread_containment
end
test "updates the user's hide_follows status", %{conn: conn} do test "updates the user's hide_follows status", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -11,6 +11,16 @@ defmodule Pleroma.Web.StreamerTest do
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
import Pleroma.Factory import Pleroma.Factory
setup do
skip_thread_containment = Pleroma.Config.get([:instance, :skip_thread_containment])
on_exit(fn ->
Pleroma.Config.put([:instance, :skip_thread_containment], skip_thread_containment)
end)
:ok
end
test "it sends to public" do test "it sends to public" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -68,6 +78,74 @@ defmodule Pleroma.Web.StreamerTest do
Task.await(task) Task.await(task)
end end
describe "thread_containment" do
test "it doesn't send to user if recipients invalid and thread containment is enabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
user = insert(:user, following: [author.ap_id])
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> refute_receive {:text, _}, 1_000 end)
fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
topics = %{"public" => [fake_socket]}
Streamer.push_to_socket(topics, "public", activity)
Task.await(task)
end
test "it sends message if recipients invalid and thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], true)
author = insert(:user)
user = insert(:user, following: [author.ap_id])
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
topics = %{"public" => [fake_socket]}
Streamer.push_to_socket(topics, "public", activity)
Task.await(task)
end
test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
user = insert(:user, following: [author.ap_id], info: %{skip_thread_containment: true})
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
topics = %{"public" => [fake_socket]}
Streamer.push_to_socket(topics, "public", activity)
Task.await(task)
end
end
test "it doesn't send to blocked users" do test "it doesn't send to blocked users" do
user = insert(:user) user = insert(:user)
blocked_user = insert(:user) blocked_user = insert(:user)

View file

@ -1495,7 +1495,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
"hide_follows" => "false" "hide_follows" => "false"
}) })
user = Repo.get!(User, user.id) user = refresh_record(user)
assert user.info.hide_follows == false assert user.info.hide_follows == false
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end end
@ -1548,6 +1548,29 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end end
test "it sets and un-sets skip_thread_containment", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{"skip_thread_containment" => "true"})
|> json_response(200)
assert response["pleroma"]["skip_thread_containment"] == true
user = refresh_record(user)
assert user.info.skip_thread_containment
response =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{"skip_thread_containment" => "false"})
|> json_response(200)
assert response["pleroma"]["skip_thread_containment"] == false
refute refresh_record(user).info.skip_thread_containment
end
test "it locks an account", %{conn: conn} do test "it locks an account", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -99,7 +99,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do
"fields" => [], "fields" => [],
"pleroma" => %{ "pleroma" => %{
"confirmation_pending" => false, "confirmation_pending" => false,
"tags" => [] "tags" => [],
"skip_thread_containment" => false
}, },
"rights" => %{"admin" => false, "delete_others_notice" => false}, "rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member" "role" => "member"
@ -154,7 +155,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do
"fields" => [], "fields" => [],
"pleroma" => %{ "pleroma" => %{
"confirmation_pending" => false, "confirmation_pending" => false,
"tags" => [] "tags" => [],
"skip_thread_containment" => false
}, },
"rights" => %{"admin" => false, "delete_others_notice" => false}, "rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member" "role" => "member"
@ -199,7 +201,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do
"fields" => [], "fields" => [],
"pleroma" => %{ "pleroma" => %{
"confirmation_pending" => false, "confirmation_pending" => false,
"tags" => [] "tags" => [],
"skip_thread_containment" => false
}, },
"rights" => %{"admin" => false, "delete_others_notice" => false}, "rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member" "role" => "member"
@ -281,7 +284,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do
"fields" => [], "fields" => [],
"pleroma" => %{ "pleroma" => %{
"confirmation_pending" => false, "confirmation_pending" => false,
"tags" => [] "tags" => [],
"skip_thread_containment" => false
}, },
"rights" => %{"admin" => false, "delete_others_notice" => false}, "rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member" "role" => "member"