local-only-fixed #138
9 changed files with 326 additions and 24 deletions
|
@ -486,9 +486,18 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
||||||
|
|
||||||
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||||
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||||
|
includes_local_public = Map.get(opts, :includes_local_public, false)
|
||||||
|
|
||||||
opts = Map.delete(opts, :user)
|
opts = Map.delete(opts, :user)
|
||||||
|
|
||||||
|
intended_recipients =
|
||||||
|
if includes_local_public do
|
||||||
|
[Constants.as_public(), as_local_public()]
|
||||||
|
else
|
||||||
[Constants.as_public()]
|
[Constants.as_public()]
|
||||||
|
end
|
||||||
|
|
||||||
|
intended_recipients
|
||||||
|> fetch_activities_query(opts)
|
|> fetch_activities_query(opts)
|
||||||
|> restrict_unlisted(opts)
|
|> restrict_unlisted(opts)
|
||||||
|> fetch_paginated_optimized(opts, pagination)
|
|> fetch_paginated_optimized(opts, pagination)
|
||||||
|
@ -588,9 +597,11 @@ defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: tr
|
||||||
do: query
|
do: query
|
||||||
|
|
||||||
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
|
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
|
||||||
|
local_public = as_local_public()
|
||||||
|
|
||||||
from(
|
from(
|
||||||
a in query,
|
a in query,
|
||||||
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
|
where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -677,8 +688,12 @@ defp fetch_activities_for_reading_user(reading_user, params) do
|
||||||
defp user_activities_recipients(%{godmode: true}), do: []
|
defp user_activities_recipients(%{godmode: true}), do: []
|
||||||
|
|
||||||
defp user_activities_recipients(%{reading_user: reading_user}) do
|
defp user_activities_recipients(%{reading_user: reading_user}) do
|
||||||
if reading_user do
|
if not is_nil(reading_user) and reading_user.local do
|
||||||
[Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
|
[
|
||||||
|
Constants.as_public(),
|
||||||
|
as_local_public(),
|
||||||
|
reading_user.ap_id | User.following(reading_user)
|
||||||
|
]
|
||||||
else
|
else
|
||||||
[Constants.as_public()]
|
[Constants.as_public()]
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,11 +85,9 @@ def visible_for_user?(%{__struct__: module} = message, user)
|
||||||
x = [user.ap_id | User.following(user)]
|
x = [user.ap_id | User.following(user)]
|
||||||
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
|
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
|
||||||
|
|
||||||
if is_local_public?(message) do
|
user_is_local = user.local
|
||||||
user.local
|
federatable = not is_local_public?(message)
|
||||||
else
|
(is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
|
||||||
is_public?(message) || Enum.any?(x, &(&1 in y))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
||||||
|
|
|
@ -113,6 +113,8 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
||||||
|> Map.put(:muting_user, user)
|
|> Map.put(:muting_user, user)
|
||||||
|> Map.put(:reply_filtering_user, user)
|
|> Map.put(:reply_filtering_user, user)
|
||||||
|> Map.put(:instance, params[:instance])
|
|> Map.put(:instance, params[:instance])
|
||||||
|
# Restricts unfederated content to authenticated users
|
||||||
|
|> Map.put(:includes_local_public, not is_nil(user))
|
||||||
|> ActivityPub.fetch_public_activities()
|
|> ActivityPub.fetch_public_activities()
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.ChangeThreadVisibilityToBeLocalOnlyAware do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar)")
|
||||||
|
execute(update_thread_visibility())
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute(
|
||||||
|
"DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar, local_public varchar)"
|
||||||
|
)
|
||||||
|
|
||||||
|
execute(restore_thread_visibility())
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_thread_visibility do
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar, local_public varchar default '') RETURNS boolean AS $$
|
||||||
|
DECLARE
|
||||||
|
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
|
||||||
|
child objects%ROWTYPE;
|
||||||
|
activity activities%ROWTYPE;
|
||||||
|
author_fa varchar;
|
||||||
|
valid_recipients varchar[];
|
||||||
|
actor_user_following varchar[];
|
||||||
|
BEGIN
|
||||||
|
--- Fetch actor following
|
||||||
|
SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships
|
||||||
|
JOIN users ON users.id = following_relationships.follower_id
|
||||||
|
JOIN users AS following ON following.id = following_relationships.following_id
|
||||||
|
WHERE users.ap_id = actor;
|
||||||
|
|
||||||
|
--- Fetch our initial activity.
|
||||||
|
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
|
||||||
|
|
||||||
|
LOOP
|
||||||
|
--- Ensure that we have an activity before continuing.
|
||||||
|
--- If we don't, the thread is not satisfiable.
|
||||||
|
IF activity IS NULL THEN
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- We only care about Create activities.
|
||||||
|
IF activity.data->>'type' != 'Create' THEN
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Normalize the child object into child.
|
||||||
|
SELECT * INTO child FROM objects
|
||||||
|
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
|
||||||
|
|
||||||
|
--- Fetch the author's AS2 following collection.
|
||||||
|
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
|
||||||
|
|
||||||
|
--- Prepare valid recipients array.
|
||||||
|
valid_recipients := ARRAY[actor, public];
|
||||||
|
--- If we specified local public, add it.
|
||||||
|
IF local_public <> '' THEN
|
||||||
|
valid_recipients := valid_recipients || local_public;
|
||||||
|
END IF;
|
||||||
|
IF ARRAY[author_fa] && actor_user_following THEN
|
||||||
|
valid_recipients := valid_recipients || author_fa;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Check visibility.
|
||||||
|
IF NOT valid_recipients && activity.recipients THEN
|
||||||
|
--- activity not visible, break out of the loop
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- If there's a parent, load it and do this all over again.
|
||||||
|
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
|
||||||
|
SELECT * INTO activity FROM activities
|
||||||
|
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE child.data->>'inReplyTo' = objects.data->>'id';
|
||||||
|
ELSE
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# priv/repo/migrations/20191007073319_create_following_relationships.exs
|
||||||
|
def restore_thread_visibility do
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$
|
||||||
|
DECLARE
|
||||||
|
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
|
||||||
|
child objects%ROWTYPE;
|
||||||
|
activity activities%ROWTYPE;
|
||||||
|
author_fa varchar;
|
||||||
|
valid_recipients varchar[];
|
||||||
|
actor_user_following varchar[];
|
||||||
|
BEGIN
|
||||||
|
--- Fetch actor following
|
||||||
|
SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships
|
||||||
|
JOIN users ON users.id = following_relationships.follower_id
|
||||||
|
JOIN users AS following ON following.id = following_relationships.following_id
|
||||||
|
WHERE users.ap_id = actor;
|
||||||
|
|
||||||
|
--- Fetch our initial activity.
|
||||||
|
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
|
||||||
|
|
||||||
|
LOOP
|
||||||
|
--- Ensure that we have an activity before continuing.
|
||||||
|
--- If we don't, the thread is not satisfiable.
|
||||||
|
IF activity IS NULL THEN
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- We only care about Create activities.
|
||||||
|
IF activity.data->>'type' != 'Create' THEN
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Normalize the child object into child.
|
||||||
|
SELECT * INTO child FROM objects
|
||||||
|
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
|
||||||
|
|
||||||
|
--- Fetch the author's AS2 following collection.
|
||||||
|
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
|
||||||
|
|
||||||
|
--- Prepare valid recipients array.
|
||||||
|
valid_recipients := ARRAY[actor, public];
|
||||||
|
IF ARRAY[author_fa] && actor_user_following THEN
|
||||||
|
valid_recipients := valid_recipients || author_fa;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Check visibility.
|
||||||
|
IF NOT valid_recipients && activity.recipients THEN
|
||||||
|
--- activity not visible, break out of the loop
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- If there's a parent, load it and do this all over again.
|
||||||
|
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
|
||||||
|
SELECT * INTO activity FROM activities
|
||||||
|
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE child.data->>'inReplyTo' = objects.data->>'id';
|
||||||
|
ELSE
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
|
@ -247,6 +247,27 @@ test "returns local-only objects when authenticated", %{conn: conn} do
|
||||||
assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
|
assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not return local-only objects for remote users", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
reader = insert(:user, local: false)
|
||||||
|
|
||||||
|
{:ok, post} =
|
||||||
|
CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
|
||||||
|
|
||||||
|
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
|
||||||
|
|
||||||
|
object = Object.normalize(post, fetch: false)
|
||||||
|
uuid = String.split(object.data["id"], "/") |> List.last()
|
||||||
|
|
||||||
|
assert response =
|
||||||
|
conn
|
||||||
|
|> assign(:user, reader)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/objects/#{uuid}")
|
||||||
|
|
||||||
|
json_response(response, 404)
|
||||||
|
end
|
||||||
|
|
||||||
test "it returns a json representation of the object with accept application/json", %{
|
test "it returns a json representation of the object with accept application/json", %{
|
||||||
conn: conn
|
conn: conn
|
||||||
} do
|
} do
|
||||||
|
@ -1297,6 +1318,35 @@ test "it returns 200 even if there're no activities", %{conn: conn} do
|
||||||
assert outbox_endpoint == result["id"]
|
assert outbox_endpoint == result["id"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it returns a local note activity when authenticated as local user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
reader = insert(:user)
|
||||||
|
{:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
|
||||||
|
ap_id = note_activity.data["id"]
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> assign(:user, reader)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not return a local note activity when unauthenticated", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert %{"orderedItems" => []} = resp
|
||||||
|
end
|
||||||
|
|
||||||
test "it returns a note activity in a collection", %{conn: conn} do
|
test "it returns a note activity in a collection", %{conn: conn} do
|
||||||
note_activity = insert(:note_activity)
|
note_activity = insert(:note_activity)
|
||||||
note_object = Object.normalize(note_activity, fetch: false)
|
note_object = Object.normalize(note_activity, fetch: false)
|
||||||
|
|
|
@ -414,6 +414,20 @@ test "unimplemented pinned statuses feature", %{conn: conn} do
|
||||||
assert json_response_and_validate_schema(conn, 200) == []
|
assert json_response_and_validate_schema(conn, 200) == []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "gets local-only statuses for authenticated users", %{user: _user, conn: conn} do
|
||||||
|
user_one = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!", visibility: "local"})
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = resp
|
||||||
|
assert id == to_string(activity.id)
|
||||||
|
end
|
||||||
|
|
||||||
test "gets an users media, excludes reblogs", %{conn: conn} do
|
test "gets an users media, excludes reblogs", %{conn: conn} do
|
||||||
note = insert(:note_activity)
|
note = insert(:note_activity)
|
||||||
user = User.get_cached_by_ap_id(note.data["actor"])
|
user = User.get_cached_by_ap_id(note.data["actor"])
|
||||||
|
|
|
@ -67,9 +67,11 @@ test "a filter with expires_in", %{conn: conn, user: user} do
|
||||||
expires_at =
|
expires_at =
|
||||||
NaiveDateTime.utc_now()
|
NaiveDateTime.utc_now()
|
||||||
|> NaiveDateTime.add(in_seconds)
|
|> NaiveDateTime.add(in_seconds)
|
||||||
|> Pleroma.Web.CommonAPI.Utils.to_masto_date()
|
|
||||||
|
|
||||||
assert response["expires_at"] == expires_at
|
assert NaiveDateTime.diff(
|
||||||
|
NaiveDateTime.from_iso8601!(response["expires_at"]),
|
||||||
|
expires_at
|
||||||
|
) < 5
|
||||||
|
|
||||||
filter = Filter.get(response["id"], user)
|
filter = Filter.get(response["id"], user)
|
||||||
|
|
||||||
|
|
|
@ -1855,6 +1855,7 @@ test "expires_at is nil for another user" do
|
||||||
|> json_response_and_validate_schema(:ok)
|
|> json_response_and_validate_schema(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "local-only statuses" do
|
||||||
test "posting a local only status" do
|
test "posting a local only status" do
|
||||||
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
|
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
|
||||||
|
|
||||||
|
@ -1874,6 +1875,32 @@ test "posting a local only status" do
|
||||||
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
|
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "other users can read local-only posts" do
|
||||||
|
user = insert(:user)
|
||||||
|
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
|
||||||
|
|
||||||
|
received =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/statuses/#{activity.id}")
|
||||||
|
|> json_response_and_validate_schema(:ok)
|
||||||
|
|
||||||
|
assert received["id"] == activity.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "anonymous users cannot see local-only posts" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
|
||||||
|
|
||||||
|
_received =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/statuses/#{activity.id}")
|
||||||
|
|> json_response_and_validate_schema(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "muted reactions" do
|
describe "muted reactions" do
|
||||||
test "index" do
|
test "index" do
|
||||||
%{conn: conn, user: user} = oauth_access(["read:statuses"])
|
%{conn: conn, user: user} = oauth_access(["read:statuses"])
|
||||||
|
|
|
@ -367,6 +367,47 @@ test "muted emotions", %{conn: conn} do
|
||||||
}
|
}
|
||||||
] = result
|
] = result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should return local-only posts for authenticated users" do
|
||||||
|
user = insert(:user)
|
||||||
|
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
|
||||||
|
|
||||||
|
{:ok, %{id: id}} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/timelines/public")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"id" => ^id}] = result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not return local-only posts for users without read:statuses" do
|
||||||
|
user = insert(:user)
|
||||||
|
%{user: _reader, conn: conn} = oauth_access([])
|
||||||
|
|
||||||
|
{:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/timelines/public")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [] = result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not return local-only posts for anonymous users" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
|
||||||
|
|
||||||
|
result =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/timelines/public")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [] = result
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp local_and_remote_activities do
|
defp local_and_remote_activities do
|
||||||
|
|
Loading…
Reference in a new issue