1
0
Fork 0
forked from mirrors/akkoma

Merge branch 'refactor/following-relationships' into 'develop'

Move following relationships to a separate table

Closes #1233

See merge request pleroma/pleroma!1814
This commit is contained in:
lain 2019-10-28 07:27:45 +00:00
commit 2480200759
33 changed files with 537 additions and 248 deletions

View file

@ -39,10 +39,12 @@ def query_timelines(user) do
"muting_user" => user
}
following = User.following(user)
Benchee.run(%{
"User home timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
[user.ap_id | user.following],
following,
home_timeline_params
)
end,
@ -60,7 +62,7 @@ def query_timelines(user) do
home_activities =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
[user.ap_id | user.following],
following,
home_timeline_params
)

View file

@ -45,15 +45,13 @@ defp generate_user_data(i) do
%{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
following: [ap_id]
following_address: ap_id <> "/following"
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(user)]
following_address: User.ap_following(user)
}
end

View file

@ -52,9 +52,9 @@ def run(["bump_all_conversations"]) do
def run(["update_users_following_followers_counts"]) do
start_pleroma()
users = Repo.all(User)
Enum.each(users, &User.remove_duplicated_following/1)
Enum.each(users, &User.update_follower_count/1)
User
|> Repo.all()
|> Enum.each(&User.update_follower_count/1)
end
def run(["prune_objects" | args]) do

View file

@ -163,7 +163,7 @@ def run(["unsubscribe", nickname]) do
user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
if Enum.empty?(User.get_friends(user)) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
end
else

View file

@ -97,7 +97,7 @@ def handle_command(state, "home") do
|> Map.put("user", user)
activities =
[user.ap_id | user.following]
[user.ap_id | Pleroma.User.following(user)]
|> ActivityPub.fetch_activities(params)
Enum.each(activities, fn activity ->

View file

@ -0,0 +1,110 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationship do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias FlakeId.Ecto.CompatType
alias Pleroma.Repo
alias Pleroma.User
schema "following_relationships" do
field(:state, :string, default: "accept")
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
timestamps()
end
def changeset(%__MODULE__{} = following_relationship, attrs) do
following_relationship
|> cast(attrs, [:state])
|> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following])
end
def get(%User{} = follower, %User{} = following) do
__MODULE__
|> where(follower_id: ^follower.id, following_id: ^following.id)
|> Repo.one()
end
def update(follower, following, "reject"), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do
nil ->
follow(follower, following, state)
following_relationship ->
following_relationship
|> cast(%{state: state}, [:state])
|> validate_required([:state])
|> Repo.update()
end
end
def follow(%User{} = follower, %User{} = following, state \\ "accept") do
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing)
end
def unfollow(%User{} = follower, %User{} = following) do
case get(follower, following) do
nil -> {:ok, nil}
%__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
end
end
def follower_count(%User{} = user) do
%{followers: user, deactivated: false}
|> User.Query.build()
|> Repo.aggregate(:count, :id)
end
def following_count(%User{id: nil}), do: 0
def following_count(%User{} = user) do
%{friends: user, deactivated: false}
|> User.Query.build()
|> Repo.aggregate(:count, :id)
end
def get_follow_requests(%User{id: id}) do
__MODULE__
|> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == "pending")
|> where([r], r.following_id == ^id)
|> select([r, f], f)
|> Repo.all()
end
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept")
|> Repo.exists?()
end
def following(%User{} = user) do
following =
__MODULE__
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|> where([r], r.follower_id == ^user.id)
|> where([r], r.state == "accept")
|> select([r, u], u.follower_address)
|> Repo.all()
if not user.local or user.nickname in [nil, "internal.fetch"] do
following
else
[user.follower_address | following]
end
end
end

View file

@ -13,6 +13,7 @@ defmodule Pleroma.User do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.FollowingRelationship
alias Pleroma.Keys
alias Pleroma.Notification
alias Pleroma.Object
@ -50,7 +51,6 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
@ -216,13 +216,7 @@ def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true)
end
def following_count(%User{following: []}), do: 0
def following_count(%User{} = user) do
user
|> get_friends_query()
|> Repo.aggregate(:count, :id)
end
defdelegate following_count(user), to: FollowingRelationship
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
@ -309,7 +303,6 @@ def update_changeset(struct, params \\ %{}) do
:bio,
:name,
:avatar,
:following,
:locked,
:no_rich_text,
:default_scope,
@ -454,7 +447,6 @@ defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
end
@ -508,8 +500,8 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true}) do
{:ok, follower}
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, "pending")
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@ -527,37 +519,22 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followed_addresses =
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.map(fn %{follower_address: fa} -> fa end)
followeds =
Enum.reject(followeds, fn followed ->
blocks?(follower, followed) || blocks?(followed, follower)
end)
q =
from(u in User,
where: u.id == ^follower.id,
update: [
set: [
following:
fragment(
"array(select distinct unnest (array_cat(?, ?)))",
u.following,
^followed_addresses
)
]
],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, &follow(follower, &1, "accept"))
Enum.each(followeds, &update_follower_count/1)
set_cache(follower)
end
def follow(%User{} = follower, %User{} = followed) do
defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
ap_followers = followed.follower_address
cond do
followed.deactivated ->
@ -567,14 +544,7 @@ def follow(%User{} = follower, %User{} = followed) do
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
q =
from(u in User,
where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
FollowingRelationship.follow(follower, followed, state)
follower = maybe_update_following_count(follower)
@ -585,17 +555,8 @@ def follow(%User{} = follower, %User{} = followed) do
end
def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
q =
from(u in User,
where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
FollowingRelationship.unfollow(follower, followed)
follower = maybe_update_following_count(follower)
@ -609,10 +570,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
end
end
@spec following?(User.t(), User.t()) :: boolean
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
defdelegate following?(follower, followed), to: FollowingRelationship
def locked?(%User{} = user) do
user.locked || false
@ -834,16 +792,7 @@ def get_friends_ids(user, page \\ nil) do
|> Repo.all()
end
@spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
def get_follow_requests(%User{} = user) do
user
|> Activity.follow_requests_for_actor()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()
end
defdelegate get_follow_requests(user), to: FollowingRelationship
def increase_note_count(%User{} = user) do
User
@ -995,18 +944,6 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user)
def increment_unread_conversation_count(_, user), do: {:ok, user}
def remove_duplicated_following(%User{following: following} = user) do
uniq_following = Enum.uniq(following)
if length(following) == length(uniq_following) do
{:ok, user}
else
user
|> update_changeset(%{following: uniq_following})
|> update_and_set_cache()
end
end
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do
criteria = %{ap_id: ap_ids, deactivated: false}

View file

@ -28,6 +28,8 @@ defmodule Pleroma.User.Query do
"""
import Ecto.Query
import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1]
alias Pleroma.FollowingRelationship
alias Pleroma.User
@type criteria ::
@ -139,18 +141,40 @@ defp compose_query({:deactivated, true}, query) do
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do
where(query, [u], fragment("? <@ ?", ^[follower_address], u.following))
defp compose_query({:followers, %User{id: id}}, query) do
query
|> where([u], u.id != ^id)
|> join(:inner, [u], r in FollowingRelationship,
as: :relationships,
on: r.following_id == ^id and r.follower_id == u.id
)
|> where([relationships: r], r.state == "accept")
end
defp compose_query({:friends, %User{id: id, following: following}}, query) do
where(query, [u], u.follower_address in ^following)
defp compose_query({:friends, %User{id: id}}, query) do
query
|> where([u], u.id != ^id)
|> join(:inner, [u], r in FollowingRelationship,
as: :relationships,
on: r.following_id == u.id and r.follower_id == ^id
)
|> where([relationships: r], r.state == "accept")
end
defp compose_query({:recipients_from_activity, to}, query) do
where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
query
|> join(:left, [u], r in FollowingRelationship,
as: :relationships,
on: r.follower_id == u.id
)
|> join(:left, [relationships: r], f in User,
as: :following,
on: f.id == r.following_id
)
|> where(
[u, following: f, relationships: r],
u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept")
)
end
defp compose_query({:order_by, key}, query) do

View file

@ -518,7 +518,9 @@ defp fetch_activities_for_context_query(context, opts) do
public = [Pleroma.Constants.as_public()]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
if opts["user"],
do: [opts["user"].ap_id | User.following(opts["user"])] ++ public,
else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
@ -712,7 +714,7 @@ defp user_activities_recipients(%{"godmode" => true}) do
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following]
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
else
[Pleroma.Constants.as_public()]
end

View file

@ -319,12 +319,12 @@ def read_inbox(
when page? in [true, "true"] do
activities =
if params["max_id"] do
ActivityPub.fetch_activities([user.ap_id | user.following], %{
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
"max_id" => params["max_id"],
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
end
conn

View file

@ -57,9 +57,10 @@ def publish(_), do: {:error, "Not implemented"}
@spec list() :: {:ok, [String.t()]} | {:error, any()}
def list do
with %User{following: following} = _user <- get_actor() do
with %User{} = user <- get_actor() do
list =
following
user
|> User.following()
|> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq()

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
alias Pleroma.FollowingRelationship
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
@ -474,7 +475,8 @@ def handle_incoming(
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
@ -484,6 +486,7 @@ def handle_incoming(
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -494,6 +497,7 @@ def handle_incoming(
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -522,7 +526,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _follower} = User.follow(follower, followed) do
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@ -545,6 +549,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, activity} <-
ActivityPub.reject(%{
to: follow_activity.data["to"],
@ -554,8 +559,6 @@ def handle_incoming(
local: false,
activity_id: id
}) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
@ -1061,43 +1064,22 @@ def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q =
from(
u in User,
where: ^old_follower_address in u.following,
update: [
set: [
following:
fragment(
"array_replace(?,?,?)",
u.following,
^old_follower_address,
^user.follower_address
)
]
from(
a in Activity,
where: ^old_follower_address in a.recipients,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
)
Repo.update_all(q, [])
q =
from(
a in Activity,
where: ^old_follower_address in a.recipients,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
]
)
|> Repo.update_all([])
end
def upgrade_user_from_ap_id(ap_id) do

View file

@ -59,7 +59,7 @@ def visible_for_user?(activity, nil) do
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@ -40,6 +41,7 @@ def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
@ -54,6 +56,7 @@ def accept_follow_request(follower, followed) do
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
@ -28,7 +29,7 @@ def home(%{assigns: %{user: user}} = conn, params) do
|> Map.put("muting_user", user)
|> Map.put("user", user)
recipients = [user.ap_id | user.following]
recipients = [user.ap_id | User.following(user)]
activities =
recipients
@ -128,9 +129,12 @@ def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
user_following = User.following(user)
activities =
following
|> Enum.filter(fn x -> x in user.following end)
|> Enum.filter(fn x -> x in user_following end)
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()

View file

@ -126,7 +126,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
recipients =
if for_user do
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | User.following(for_user)]
else
[Pleroma.Constants.as_public()]
end

View file

@ -0,0 +1,149 @@
defmodule Pleroma.Repo.Migrations.CreateFollowingRelationships do
use Ecto.Migration
def change do
create_if_not_exists table(:following_relationships) do
add(:follower_id, references(:users, type: :uuid, on_delete: :delete_all), null: false)
add(:following_id, references(:users, type: :uuid, on_delete: :delete_all), null: false)
add(:state, :string, null: false)
timestamps()
end
create_if_not_exists(index(:following_relationships, :follower_id))
create_if_not_exists(unique_index(:following_relationships, [:follower_id, :following_id]))
execute(update_thread_visibility(), restore_thread_visibility())
end
# The only difference between the original version: `actor_user` replaced with `actor_user_following`
def update_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
# priv/repo/migrations/20190515222404_add_thread_visibility_function.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;
actor_user users%ROWTYPE;
author_fa varchar;
valid_recipients varchar[];
BEGIN
--- Fetch our actor.
SELECT * INTO actor_user FROM users 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

View file

@ -0,0 +1,89 @@
defmodule Pleroma.Repo.Migrations.MigrateFollowingRelationships do
use Ecto.Migration
def change do
execute(import_following_from_users(), "")
execute(import_following_from_activities(), restore_following_column())
end
defp import_following_from_users do
"""
INSERT INTO following_relationships (follower_id, following_id, state, inserted_at, updated_at)
SELECT
relations.follower_id,
following.id,
'accept',
now(),
now()
FROM (
SELECT
users.id AS follower_id,
unnest(users.following) AS following_ap_id
FROM
users
WHERE
users.following != '{}'
AND users.local = false OR users.local = true AND users.email IS NOT NULL -- Exclude `internal/fetch` and `relay`
) AS relations
JOIN users AS "following" ON "following".follower_address = relations.following_ap_id
WHERE relations.follower_id != following.id
ON CONFLICT DO NOTHING
"""
end
defp import_following_from_activities do
"""
INSERT INTO
following_relationships (
follower_id,
following_id,
state,
inserted_at,
updated_at
)
SELECT
followers.id,
following.id,
activities.data ->> 'state',
(activities.data ->> 'published') :: timestamp,
now()
FROM
activities
JOIN users AS followers ON (activities.actor = followers.ap_id)
JOIN users AS following ON (activities.data ->> 'object' = following.ap_id)
WHERE
activities.data ->> 'type' = 'Follow'
AND activities.data ->> 'state' IN ('accept', 'pending', 'reject')
ORDER BY activities.updated_at DESC
ON CONFLICT DO NOTHING
"""
end
defp restore_following_column do
"""
UPDATE
users
SET
following = following_query.following_array,
updated_at = now()
FROM (
SELECT
follower.id AS follower_id,
CASE follower.local
WHEN TRUE THEN
array_prepend(follower.follower_address, array_agg(following.follower_address))
ELSE
array_agg(following.follower_address)
END AS following_array
FROM
following_relationships
JOIN users AS follower ON follower.id = following_relationships.follower_id
JOIN users AS following ON following.id = following_relationships.following_id
GROUP BY
follower.id) AS following_query
WHERE
following_query.follower_id = users.id
"""
end
end

View file

@ -0,0 +1,16 @@
defmodule Pleroma.Repo.Migrations.DropUsersFollowing do
use Ecto.Migration
# had to disable these to be able to restore `following` index concurrently
# https://hexdocs.pm/ecto_sql/Ecto.Migration.html#index/3-adding-dropping-indexes-concurrently
@disable_ddl_transaction true
@disable_migration_lock true
def change do
drop(index(:users, [:following], concurrently: true, using: :gin))
alter table(:users) do
remove(:following, {:array, :string}, default: [])
end
end
end

View file

@ -39,8 +39,7 @@ def user_factory do
user
| ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(user)]
following_address: User.ap_following(user)
}
end

View file

@ -72,24 +72,25 @@ test "it prunes old objects from the database" do
describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do
[user, user2] = insert_pair(:user)
{:ok, %User{following: following} = user} = User.follow(user, user2)
{:ok, %User{} = user} = User.follow(user, user2)
following = User.following(user)
assert length(following) == 2
assert user.follower_count == 0
{:ok, user} =
user
|> Ecto.Changeset.change(%{following: following ++ following, follower_count: 3})
|> Ecto.Changeset.change(%{follower_count: 3})
|> Repo.update()
assert length(user.following) == 4
assert user.follower_count == 3
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
user = User.get_by_id(user.id)
assert length(user.following) == 2
assert length(User.following(user)) == 2
assert user.follower_count == 0
end
end

View file

@ -51,7 +51,7 @@ test "relay is unfollowed" do
target_user = User.get_cached_by_ap_id(target_instance)
follow_activity = Utils.fetch_latest_follow(local_user, target_user)
User.follow(local_user, target_user)
assert "#{target_instance}/followers" in refresh_record(local_user).following
assert "#{target_instance}/followers" in User.following(local_user)
Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance])
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
@ -68,7 +68,7 @@ test "relay is unfollowed" do
assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id
assert undo_activity.data["object"] == cancelled_activity.data
refute "#{target_instance}/followers" in refresh_record(local_user).following
refute "#{target_instance}/followers" in User.following(local_user)
end
end
@ -78,20 +78,18 @@ test "Prints relay subscription list" do
refute_receive {:mix_shell, :info, _}
Pleroma.Web.ActivityPub.Relay.get_actor()
|> Ecto.Changeset.change(
following: [
"http://test-app.com/user/test1",
"http://test-app.com/user/test1",
"http://test-app-42.com/user/test1"
]
)
|> Pleroma.User.update_and_set_cache()
relay_user = Relay.get_actor()
["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
|> Enum.each(fn ap_id ->
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
User.follow(relay_user, user)
end)
:ok = Mix.Tasks.Pleroma.Relay.run(["list"])
assert_receive {:mix_shell, :info, ["test-app.com"]}
assert_receive {:mix_shell, :info, ["test-app-42.com"]}
assert_receive {:mix_shell, :info, ["mstdn.io"]}
assert_receive {:mix_shell, :info, ["mastodon.example.org"]}
end
end
end

View file

@ -139,7 +139,8 @@ test "no user to toggle" do
describe "running unsubscribe" do
test "user is unsubscribed" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
user = insert(:user)
User.follow(user, followed, "accept")
Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
@ -154,7 +155,7 @@ test "user is unsubscribed" do
assert message =~ "Successfully unsubscribed"
user = User.get_cached_by_nickname(user.nickname)
assert Enum.empty?(user.following)
assert Enum.empty?(User.get_friends(user))
assert user.deactivated
end

View file

@ -88,10 +88,9 @@ test "doesn't return already accepted or duplicate follow requests" do
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked)
User.follow(accepted_follower, locked)
Pleroma.FollowingRelationship.update(accepted_follower, locked, "accept")
assert [activity] = User.get_follow_requests(locked)
assert activity
assert [^pending_follower] = User.get_follow_requests(locked)
end
test "clears follow requests when requester is blocked" do
@ -136,10 +135,10 @@ test "follow_all follows mutliple users without duplicating" do
followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one])
assert length(user.following) == 3
assert length(User.following(user)) == 3
{:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert length(user.following) == 4
assert length(User.following(user)) == 4
end
test "follow takes a user and another user" do
@ -153,7 +152,7 @@ test "follow takes a user and another user" do
followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.follower_count == 1
assert User.ap_followers(followed) in user.following
assert User.ap_followers(followed) in User.following(user)
end
test "can't follow a deactivated users" do
@ -218,26 +217,29 @@ test "unfollow with syncronizes external user" do
nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following",
following: [User.ap_followers(followed)]
following_address: "http://localhost:4001/users/fuser2/following"
})
{:ok, user} = User.follow(user, followed, "accept")
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert user.following == []
assert User.following(user) == []
end
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
user = insert(:user)
{:ok, user} = User.follow(user, followed, "accept")
assert User.following(user) == [user.follower_address, followed.follower_address]
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert user.following == []
assert User.following(user) == [user.follower_address]
end
test "unfollow doesn't unfollow yourself" do
@ -245,14 +247,14 @@ test "unfollow doesn't unfollow yourself" do
{:error, _} = User.unfollow(user, user)
user = User.get_cached_by_id(user.id)
assert user.following == [user.ap_id]
assert User.following(user) == [user.follower_address]
end
end
test "test if a user is following another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
user = insert(:user)
User.follow(user, followed, "accept")
assert User.following?(user, followed)
refute User.following?(followed, user)
@ -335,7 +337,7 @@ test "it restricts certain nicknames" do
refute changeset.valid?
end
test "it sets the password_hash, ap_id and following fields" do
test "it sets the password_hash and ap_id" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
@ -343,10 +345,6 @@ test "it sets the password_hash, ap_id and following fields" do
assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes[:following] == [
User.ap_followers(%User{nickname: @full_user_data.nickname})
]
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
@ -650,37 +648,6 @@ test "it sets the follower_count property" do
end
end
describe "remove duplicates from following list" do
test "it removes duplicates" do
user = insert(:user)
follower = insert(:user)
{:ok, %User{following: following} = follower} = User.follow(follower, user)
assert length(following) == 2
{:ok, follower} =
follower
|> User.update_changeset(%{following: following ++ following})
|> Repo.update()
assert length(follower.following) == 4
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
test "it does nothing when following is uniq" do
user = insert(:user)
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
assert length(follower.following) == 2
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
end
describe "follow_import" do
test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user)
@ -989,7 +956,9 @@ test "hide a user's statuses from timelines and notifications" do
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
"user" => user2
})
{:ok, _user} = User.deactivate(user)
@ -997,7 +966,9 @@ test "hide a user's statuses from timelines and notifications" do
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
"user" => user2
})
end
end

View file

@ -693,7 +693,7 @@ test "does include announces on request" do
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)])
assert announce_activity.id == announce.id
end
@ -1219,7 +1219,7 @@ test "it filters broken threads" do
})
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following])
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
@ -1229,7 +1229,7 @@ test "it filters broken threads" do
assert length(activities) == 3
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity.id, private_activity_1.id] == activities

View file

@ -56,14 +56,14 @@ test "returns activity" do
service_actor = Relay.get_actor()
ActivityPub.follow(service_actor, user)
Pleroma.User.follow(service_actor, user)
assert "#{user.ap_id}/followers" in refresh_record(service_actor).following
assert "#{user.ap_id}/followers" in User.following(service_actor)
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
assert user.ap_id in activity.recipients
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["to"] == [user.ap_id]
refute "#{user.ap_id}/followers" in refresh_record(service_actor).following
refute "#{user.ap_id}/followers" in User.following(service_actor)
end
end

View file

@ -1334,7 +1334,8 @@ test "it upgrades a user to activitypub" do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
user_two = insert(:user, %{following: [user.follower_address]})
user_two = insert(:user)
Pleroma.FollowingRelationship.follow(user_two, user, "accept")
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
@ -1381,8 +1382,8 @@ test "it upgrades a user to activitypub" do
refute user.follower_address in unrelated_activity.recipients
user_two = User.get_cached_by_id(user_two.id)
assert user.follower_address in user_two.following
refute "..." in user_two.following
assert User.following?(user_two, user)
refute "..." in User.following(user_two)
end
end

View file

@ -212,7 +212,8 @@ test "returns false when invalid recipients", %{user: user} do
test "returns true if user following to author" do
author = insert(:user)
user = insert(:user, following: [author.ap_id])
user = insert(:user)
Pleroma.User.follow(user, author)
activity =
insert(:note_activity,

View file

@ -2572,22 +2572,20 @@ test "POST /relay", %{admin: admin} do
end
test "GET /relay", %{admin: admin} do
Pleroma.Web.ActivityPub.Relay.get_actor()
|> Ecto.Changeset.change(
following: [
"http://test-app.com/user/test1",
"http://test-app.com/user/test1",
"http://test-app-42.com/user/test1"
]
)
|> Pleroma.User.update_and_set_cache()
relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
|> Enum.each(fn ap_id ->
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
User.follow(relay_user, user)
end)
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/relay")
assert json_response(conn, 200)["relays"] -- ["test-app.com", "test-app-42.com"] == []
assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
end
test "DELETE /relay", %{admin: admin} do

View file

@ -471,7 +471,7 @@ test "following without reblogs" do
conn =
build_conn()
|> assign(:user, follower)
|> assign(:user, User.get_cached_by_id(follower.id))
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
assert %{"showing_reblogs" => true} = json_response(conn, 200)

View file

@ -16,9 +16,7 @@ test "/api/v1/follow_requests works" do
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
{:ok, other_user} = User.follow(other_user, user, "pending")
assert User.following?(other_user, user) == false
@ -36,6 +34,7 @@ test "/api/v1/follow_requests/:id/authorize works" do
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
{:ok, other_user} = User.follow(other_user, user, "pending")
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)

View file

@ -169,7 +169,8 @@ test "it sends to public" 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])
user = insert(:user)
User.follow(user, author, "accept")
activity =
insert(:note_activity,
@ -191,7 +192,8 @@ test "it doesn't send to user if recipients invalid and thread containment is en
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])
user = insert(:user)
User.follow(user, author, "accept")
activity =
insert(:note_activity,
@ -213,7 +215,8 @@ test "it sends message if recipients invalid and thread containment is disabled"
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], skip_thread_containment: true)
user = insert(:user, skip_thread_containment: true)
User.follow(user, author, "accept")
activity =
insert(:note_activity,

View file

@ -366,7 +366,7 @@ test "follows user", %{conn: conn} do
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in refresh_record(user).following
assert user2.follower_address in User.following(user)
end
test "returns error when user is deactivated", %{conn: conn} do
@ -438,7 +438,7 @@ test "follows", %{conn: conn} do
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in refresh_record(user).following
assert user2.follower_address in User.following(user)
end
test "returns error when followee not found", %{conn: conn} do