Merge branch 'develop' into issue/1276

This commit is contained in:
Maksim Pechnikov 2019-10-29 22:23:19 +03:00
commit b27a92e8fa
66 changed files with 1283 additions and 377 deletions

View File

@ -25,7 +25,7 @@ While we dont provide docker files, other people have written very good ones.
### Dependencies ### Dependencies
* Postgresql version 9.6 or newer * Postgresql version 9.6 or newer, including the contrib modules
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixirs install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf). * Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixirs install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools * Build-essential tools
@ -71,7 +71,7 @@ This is useful for running Pleroma inside Tor or I2P.
## Customization and contribution ## Customization and contribution
The [Pleroma Documentation](https://docs-develop.pleroma.social/readme.html) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project. The [Pleroma Documentation](https://docs-develop.pleroma.social) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
## Troubleshooting ## Troubleshooting

View File

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

View File

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

View File

@ -52,9 +52,9 @@ def run(["bump_all_conversations"]) do
def run(["update_users_following_followers_counts"]) do def run(["update_users_following_followers_counts"]) do
start_pleroma() start_pleroma()
users = Repo.all(User) User
Enum.each(users, &User.remove_duplicated_following/1) |> Repo.all()
Enum.each(users, &User.update_follower_count/1) |> Enum.each(&User.update_follower_count/1)
end end
def run(["prune_objects" | args]) do 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) 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}") shell_info("Successfully unsubscribed all followers from #{user.nickname}")
end end
else else

View File

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

View File

@ -67,7 +67,13 @@ def create_or_bump_for(activity, opts \\ []) do
participations = participations =
Enum.map(users, fn user -> Enum.map(users, fn user ->
User.increment_unread_conversation_count(conversation, user) invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
unless invisible_conversation do
User.increment_unread_conversation_count(conversation, user)
end
opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts) Participation.create_for_user_and_conversation(user, conversation, opts)

View File

@ -32,11 +32,20 @@ def creation_cng(struct, params) do
def create_for_user_and_conversation(user, conversation, opts \\ []) do def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read] read = !!opts[:read]
invisible_conversation = !!opts[:invisible_conversation]
update_on_conflict =
if(invisible_conversation, do: [], else: [read: read])
|> Keyword.put(:updated_at, NaiveDateTime.utc_now())
%__MODULE__{} %__MODULE__{}
|> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read}) |> creation_cng(%{
user_id: user.id,
conversation_id: conversation.id,
read: invisible_conversation || read
})
|> Repo.insert( |> Repo.insert(
on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]], on_conflict: [set: update_on_conflict],
returning: true, returning: true,
conflict_target: [:user_id, :conversation_id] conflict_target: [:user_id, :conversation_id]
) )
@ -69,7 +78,26 @@ def mark_as_read(participation) do
end end
end end
def mark_all_as_read(user) do def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
target_conversation_ids =
__MODULE__
|> where([p], p.user_id == ^target_user.id)
|> select([p], p.conversation_id)
|> Repo.all()
__MODULE__
|> where([p], p.user_id == ^user.id)
|> where([p], p.conversation_id in ^target_conversation_ids)
|> update([p], set: [read: true])
|> Repo.update_all([])
{:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, []}
end
def mark_all_as_read(%User{} = user, %User{}), do: {:ok, user, []}
def mark_all_as_read(%User{} = user) do
{_, participations} = {_, participations} =
__MODULE__ __MODULE__
|> where([p], p.user_id == ^user.id) |> where([p], p.user_id == ^user.id)
@ -78,8 +106,8 @@ def mark_all_as_read(user) do
|> select([p], p) |> select([p], p)
|> Repo.update_all([]) |> Repo.update_all([])
User.set_unread_conversation_count(user) {:ok, user} = User.set_unread_conversation_count(user)
{:ok, participations} {:ok, user, participations}
end end
def mark_as_unread(participation) do def mark_as_unread(participation) do

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

@ -90,6 +90,9 @@ def fetch_object_from_id(id, options \\ []) do
{:fetch_object, %Object{} = object} -> {:fetch_object, %Object{} = object} ->
{:ok, object} {:ok, object}
{:fetch, {:error, error}} ->
{:error, error}
e -> e ->
e e
end end
@ -110,6 +113,9 @@ def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do with {:ok, object} <- fetch_object_from_id(id, options) do
object object
else else
{:error, %Tesla.Mock.Error{}} ->
nil
e -> e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}") Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil nil
@ -170,6 +176,9 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
{:scheme, _} -> {:scheme, _} ->
{:error, "Unsupported URI scheme"} {:error, "Unsupported URI scheme"}
{:error, e} ->
{:error, e}
e -> e ->
{:error, e} {:error, e}
end end

View File

@ -13,6 +13,7 @@ defmodule Pleroma.User do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.FollowingRelationship
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -50,7 +51,6 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true) field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true) field(:password_confirmation, :string, virtual: true)
field(:keys, :string) field(:keys, :string)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map) field(:avatar, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -216,61 +216,7 @@ def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true) from(u in query, where: u.deactivated != ^true)
end end
def following_count(%User{following: []}), do: 0 defdelegate following_count(user), to: FollowingRelationship
def following_count(%User{} = user) do
user
|> get_friends_query()
|> Repo.aggregate(:count, :id)
end
@info_fields [
:banner,
:background,
:source_data,
:note_count,
:follower_count,
:following_count,
:locked,
:confirmation_pending,
:password_reset_pending,
:confirmation_token,
:default_scope,
:blocks,
:domain_blocks,
:mutes,
:muted_reblogs,
:muted_notifications,
:subscribers,
:deactivated,
:no_rich_text,
:ap_enabled,
:is_moderator,
:is_admin,
:show_role,
:settings,
:magic_key,
:uri,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:unread_conversation_count,
:pinned_activities,
:email_notifications,
:mascot,
:emoji,
:pleroma_settings_store,
:fields,
:raw_fields,
:discoverable,
:invisible,
:skip_thread_containment,
:notification_settings
]
def info_fields, do: @info_fields
defp truncate_fields_param(params) do defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do if Map.has_key?(params, :fields) do
@ -357,7 +303,6 @@ def update_changeset(struct, params \\ %{}) do
:bio, :bio,
:name, :name,
:avatar, :avatar,
:following,
:locked, :locked,
:no_rich_text, :no_rich_text,
:default_scope, :default_scope,
@ -502,7 +447,6 @@ defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset changeset
|> put_change(:following, [followers])
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
end end
@ -556,8 +500,8 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} @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 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
{:ok, follower} follow(follower, followed, "pending")
end end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@ -575,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." @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()} @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do def follow_all(follower, followeds) do
followed_addresses = followeds =
followeds Enum.reject(followeds, fn followed ->
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) blocks?(follower, followed) || blocks?(followed, follower)
|> Enum.map(fn %{follower_address: fa} -> fa end) end)
q = Enum.each(followeds, &follow(follower, &1, "accept"))
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, &update_follower_count/1) Enum.each(followeds, &update_follower_count/1)
set_cache(follower) set_cache(follower)
end 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]) deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
ap_followers = followed.follower_address
cond do cond do
followed.deactivated -> followed.deactivated ->
@ -615,14 +544,7 @@ def follow(%User{} = follower, %User{} = followed) do
{:error, "Could not follow user: #{followed.nickname} blocked you."} {:error, "Could not follow user: #{followed.nickname} blocked you."}
true -> true ->
q = FollowingRelationship.follow(follower, followed, state)
from(u in User,
where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
follower = maybe_update_following_count(follower) follower = maybe_update_following_count(follower)
@ -633,17 +555,8 @@ def follow(%User{} = follower, %User{} = followed) do
end end
def unfollow(%User{} = follower, %User{} = followed) do def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do if following?(follower, followed) and follower.ap_id != followed.ap_id do
q = FollowingRelationship.unfollow(follower, followed)
from(u in User,
where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
follower = maybe_update_following_count(follower) follower = maybe_update_following_count(follower)
@ -657,10 +570,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
end end
end end
@spec following?(User.t(), User.t()) :: boolean defdelegate following?(follower, followed), to: FollowingRelationship
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def locked?(%User{} = user) do def locked?(%User{} = user) do
user.locked || false user.locked || false
@ -882,16 +792,7 @@ def get_friends_ids(user, page \\ nil) do
|> Repo.all() |> Repo.all()
end end
@spec get_follow_requests(User.t()) :: {:ok, [User.t()]} defdelegate get_follow_requests(user), to: FollowingRelationship
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
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
User User
@ -1019,7 +920,7 @@ def set_unread_conversation_count(%User{local: true} = user) do
end end
end end
def set_unread_conversation_count(_), do: :noop def set_unread_conversation_count(user), do: {:ok, user}
def increment_unread_conversation_count(conversation, %User{local: true} = user) do def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query = unread_query =
@ -1041,19 +942,7 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user)
end end
end end
def increment_unread_conversation_count(_, _), do: :noop 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()] @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do def get_users_from_set(ap_ids, local_only \\ true) do
@ -1125,7 +1014,7 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
if following?(blocked, blocker), do: unfollow(blocked, blocker) if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker) {:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, ap_id) add_to_block(blocker, ap_id)
end end

View File

@ -28,6 +28,8 @@ defmodule Pleroma.User.Query do
""" """
import Ecto.Query import Ecto.Query
import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1]
alias Pleroma.FollowingRelationship
alias Pleroma.User alias Pleroma.User
@type criteria :: @type criteria ::
@ -139,18 +141,40 @@ defp compose_query({:deactivated, true}, query) do
|> where([u], not is_nil(u.nickname)) |> where([u], not is_nil(u.nickname))
end end
defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do defp compose_query({:followers, %User{id: id}}, query) do
where(query, [u], fragment("? <@ ?", ^[follower_address], u.following)) query
|> where([u], u.id != ^id) |> 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 end
defp compose_query({:friends, %User{id: id, following: following}}, query) do defp compose_query({:friends, %User{id: id}}, query) do
where(query, [u], u.follower_address in ^following) query
|> where([u], u.id != ^id) |> 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 end
defp compose_query({:recipients_from_activity, to}, query) do 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 end
defp compose_query({:order_by, key}, query) do 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()] public = [Pleroma.Constants.as_public()]
recipients = 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) from(activity in Activity)
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
@ -712,7 +714,7 @@ 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 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 else
[Pleroma.Constants.as_public()] [Pleroma.Constants.as_public()]
end end

View File

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

View File

@ -57,9 +57,10 @@ def publish(_), do: {:error, "Not implemented"}
@spec list() :: {:ok, [String.t()]} | {:error, any()} @spec list() :: {:ok, [String.t()]} | {:error, any()}
def list do def list do
with %User{following: following} = _user <- get_actor() do with %User{} = user <- get_actor() do
list = list =
following user
|> User.following()
|> Enum.map(fn entry -> URI.parse(entry).host end) |> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq() |> 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. A module to handle coding from internal to wire ActivityPub and back.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.FollowingRelationship
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
@ -474,7 +475,8 @@ def handle_incoming(
{_, false} <- {:user_locked, User.locked?(followed)}, {_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <- {_, {: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(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
actor: followed, actor: followed,
@ -484,6 +486,7 @@ def handle_incoming(
else else
{:user_blocked, true} -> {:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -494,6 +497,7 @@ def handle_incoming(
{:follow, {:error, _}} -> {:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -503,6 +507,7 @@ def handle_incoming(
}) })
{:user_locked, true} -> {:user_locked, true} ->
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
:noop :noop
end end
@ -522,7 +527,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {: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"]), %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(%{ ActivityPub.accept(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
type: "Accept", type: "Accept",
@ -545,6 +550,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {: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"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, activity} <- {:ok, activity} <-
ActivityPub.reject(%{ ActivityPub.reject(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
@ -554,8 +560,6 @@ def handle_incoming(
local: false, local: false,
activity_id: id activity_id: id
}) do }) do
User.unfollow(follower, followed)
{:ok, activity} {:ok, activity}
else else
_e -> :error _e -> :error
@ -1061,43 +1065,22 @@ def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away # we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname}) old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q = from(
from( a in Activity,
u in User, where: ^old_follower_address in a.recipients,
where: ^old_follower_address in u.following, update: [
update: [ set: [
set: [ recipients:
following: fragment(
fragment( "array_replace(?,?,?)",
"array_replace(?,?,?)", a.recipients,
u.following, ^old_follower_address,
^old_follower_address, ^user.follower_address
^user.follower_address )
)
]
] ]
) ]
)
Repo.update_all(q, []) |> Repo.update_all([])
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, [])
end end
def upgrade_user_from_ap_id(ap_id) do def upgrade_user_from_ap_id(ap_id) do

View File

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

View File

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

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) 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("muting_user", user)
|> Map.put("user", user) |> Map.put("user", user)
recipients = [user.ap_id | user.following] recipients = [user.ap_id | User.following(user)]
activities = activities =
recipients 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 # 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). # does not actually have permission to see (for more info, peruse security issue #270).
user_following = User.following(user)
activities = activities =
following 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) |> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse() |> Enum.reverse()

View File

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

View File

@ -80,7 +80,7 @@ def update_conversation(
end end
def read_conversations(%{assigns: %{user: user}} = conn, _params) do def read_conversations(%{assigns: %{user: user}} = conn, _params) do
with {:ok, participations} <- Participation.mark_all_as_read(user) do with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn conn
|> add_link_headers(participations) |> add_link_headers(participations)
|> put_view(ConversationView) |> put_view(ConversationView)

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

@ -0,0 +1,53 @@
defmodule Pleroma.Repo.Migrations.AddUsersInfoColumns do
use Ecto.Migration
@jsonb_array_default "'[]'::jsonb"
def change do
alter table(:users) do
add_if_not_exists(:banner, :map, default: %{})
add_if_not_exists(:background, :map, default: %{})
add_if_not_exists(:source_data, :map, default: %{})
add_if_not_exists(:note_count, :integer, default: 0)
add_if_not_exists(:follower_count, :integer, default: 0)
add_if_not_exists(:following_count, :integer, default: nil)
add_if_not_exists(:locked, :boolean, default: false, null: false)
add_if_not_exists(:confirmation_pending, :boolean, default: false, null: false)
add_if_not_exists(:password_reset_pending, :boolean, default: false, null: false)
add_if_not_exists(:confirmation_token, :text, default: nil)
add_if_not_exists(:default_scope, :string, default: "public")
add_if_not_exists(:blocks, {:array, :text}, default: [])
add_if_not_exists(:domain_blocks, {:array, :text}, default: [])
add_if_not_exists(:mutes, {:array, :text}, default: [])
add_if_not_exists(:muted_reblogs, {:array, :text}, default: [])
add_if_not_exists(:muted_notifications, {:array, :text}, default: [])
add_if_not_exists(:subscribers, {:array, :text}, default: [])
add_if_not_exists(:deactivated, :boolean, default: false, null: false)
add_if_not_exists(:no_rich_text, :boolean, default: false, null: false)
add_if_not_exists(:ap_enabled, :boolean, default: false, null: false)
add_if_not_exists(:is_moderator, :boolean, default: false, null: false)
add_if_not_exists(:is_admin, :boolean, default: false, null: false)
add_if_not_exists(:show_role, :boolean, default: true, null: false)
add_if_not_exists(:settings, :map, default: nil)
add_if_not_exists(:magic_key, :text, default: nil)
add_if_not_exists(:uri, :text, default: nil)
add_if_not_exists(:hide_followers_count, :boolean, default: false, null: false)
add_if_not_exists(:hide_follows_count, :boolean, default: false, null: false)
add_if_not_exists(:hide_followers, :boolean, default: false, null: false)
add_if_not_exists(:hide_follows, :boolean, default: false, null: false)
add_if_not_exists(:hide_favorites, :boolean, default: true, null: false)
add_if_not_exists(:unread_conversation_count, :integer, default: 0)
add_if_not_exists(:pinned_activities, {:array, :text}, default: [])
add_if_not_exists(:email_notifications, :map, default: %{"digest" => false})
add_if_not_exists(:mascot, :map, default: nil)
add_if_not_exists(:emoji, :map, default: fragment(@jsonb_array_default))
add_if_not_exists(:pleroma_settings_store, :map, default: %{})
add_if_not_exists(:fields, :map, default: fragment(@jsonb_array_default))
add_if_not_exists(:raw_fields, :map, default: fragment(@jsonb_array_default))
add_if_not_exists(:discoverable, :boolean, default: false, null: false)
add_if_not_exists(:invisible, :boolean, default: false, null: false)
add_if_not_exists(:notification_settings, :map, default: %{})
add_if_not_exists(:skip_thread_containment, :boolean, default: false, null: false)
end
end
end

View File

@ -95,79 +95,37 @@ defmodule Pleroma.Repo.Migrations.CopyUsersInfoFieldsToUsers do
] ]
def change do def change do
alter table(:users) do
add(:banner, :map, default: %{})
add(:background, :map, default: %{})
add(:source_data, :map, default: %{})
add(:note_count, :integer, default: 0)
add(:follower_count, :integer, default: 0)
add(:following_count, :integer, default: nil)
add(:locked, :boolean, default: false, null: false)
add(:confirmation_pending, :boolean, default: false, null: false)
add(:password_reset_pending, :boolean, default: false, null: false)
add(:confirmation_token, :text, default: nil)
add(:default_scope, :string, default: "public")
add(:blocks, {:array, :text}, default: [])
add(:domain_blocks, {:array, :text}, default: [])
add(:mutes, {:array, :text}, default: [])
add(:muted_reblogs, {:array, :text}, default: [])
add(:muted_notifications, {:array, :text}, default: [])
add(:subscribers, {:array, :text}, default: [])
add(:deactivated, :boolean, default: false, null: false)
add(:no_rich_text, :boolean, default: false, null: false)
add(:ap_enabled, :boolean, default: false, null: false)
add(:is_moderator, :boolean, default: false, null: false)
add(:is_admin, :boolean, default: false, null: false)
add(:show_role, :boolean, default: true, null: false)
add(:settings, :map, default: nil)
add(:magic_key, :text, default: nil)
add(:uri, :text, default: nil)
add(:hide_followers_count, :boolean, default: false, null: false)
add(:hide_follows_count, :boolean, default: false, null: false)
add(:hide_followers, :boolean, default: false, null: false)
add(:hide_follows, :boolean, default: false, null: false)
add(:hide_favorites, :boolean, default: true, null: false)
add(:unread_conversation_count, :integer, default: 0)
add(:pinned_activities, {:array, :text}, default: [])
add(:email_notifications, :map, default: %{"digest" => false})
add(:mascot, :map, default: nil)
add(:emoji, :map, default: fragment(@jsonb_array_default))
add(:pleroma_settings_store, :map, default: %{})
add(:fields, :map, default: fragment(@jsonb_array_default))
add(:raw_fields, :map, default: fragment(@jsonb_array_default))
add(:discoverable, :boolean, default: false, null: false)
add(:invisible, :boolean, default: false, null: false)
add(:notification_settings, :map, default: %{})
add(:skip_thread_containment, :boolean, default: false, null: false)
end
if direction() == :up do if direction() == :up do
for f <- @info_fields do sets =
set_field = "update users set #{f} =" for f <- @info_fields do
set_field = "#{f} ="
# Coercion of null::jsonb to NULL # Coercion of null::jsonb to NULL
jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end" jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end"
cond do cond do
f in @jsonb_fields -> f in @jsonb_fields ->
execute("#{set_field} #{jsonb}") "#{set_field} #{jsonb}"
f in @array_jsonb_fields -> f in @array_jsonb_fields ->
execute("#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})") "#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})"
f in @int_fields -> f in @int_fields ->
execute("#{set_field} (info->>'#{f}')::int") "#{set_field} (info->>'#{f}')::int"
f in @boolean_fields -> f in @boolean_fields ->
execute("#{set_field} coalesce((info->>'#{f}')::boolean, false)") "#{set_field} coalesce((info->>'#{f}')::boolean, false)"
f in @array_text_fields -> f in @array_text_fields ->
execute("#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))") "#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))"
true -> true ->
execute("#{set_field} info->>'#{f}'") "#{set_field} info->>'#{f}'"
end
end end
end |> Enum.join(", ")
execute("update users set " <> sets)
for index_name <- [ for index_name <- [
:users_deactivated_index, :users_deactivated_index,

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForActivities do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE activities
ALTER COLUMN data SET NOT NULL,
ALTER COLUMN local SET NOT NULL")
end
def down do
execute("ALTER TABLE activities
ALTER COLUMN data DROP NOT NULL,
ALTER COLUMN local DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForActivityExpirations do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE activity_expirations
ALTER COLUMN activity_id SET NOT NULL")
end
def down do
execute("ALTER TABLE activity_expirations
ALTER COLUMN activity_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForApps do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE apps
ALTER COLUMN client_name SET NOT NULL,
ALTER COLUMN redirect_uris SET NOT NULL")
end
def down do
execute("ALTER TABLE apps
ALTER COLUMN client_name DROP NOT NULL,
ALTER COLUMN redirect_uris DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForBookmarks do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE bookmarks
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN activity_id SET NOT NULL")
end
def down do
execute("ALTER TABLE bookmarks
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN activity_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForConfig do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE config
ALTER COLUMN key SET NOT NULL,
ALTER COLUMN value SET NOT NULL")
end
def down do
execute("ALTER TABLE config
ALTER COLUMN key DROP NOT NULL,
ALTER COLUMN value DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipationRecipientShips do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE conversation_participation_recipient_ships
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN participation_id SET NOT NULL")
end
def down do
execute("ALTER TABLE conversation_participation_recipient_ships
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN participation_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipations do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE conversation_participations
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN conversation_id SET NOT NULL,
ALTER COLUMN read SET NOT NULL")
end
def down do
execute("ALTER TABLE conversation_participations
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN conversation_id DROP NOT NULL,
ALTER COLUMN read DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForFilters do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE filters
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN filter_id SET NOT NULL,
ALTER COLUMN whole_word SET NOT NULL")
end
def down do
execute("ALTER TABLE filters
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN filter_id DROP NOT NULL,
ALTER COLUMN whole_word DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForInstances do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE instances
ALTER COLUMN host SET NOT NULL")
end
def down do
execute("ALTER TABLE instances
ALTER COLUMN host DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForLists do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE lists
ALTER COLUMN user_id SET NOT NULL")
end
def down do
execute("ALTER TABLE lists
ALTER COLUMN user_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForMarkers do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE markers
ALTER COLUMN user_id SET NOT NULL")
end
def down do
execute("ALTER TABLE markers
ALTER COLUMN user_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForModerationLog do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE moderation_log
ALTER COLUMN data SET NOT NULL")
end
def down do
execute("ALTER TABLE moderation_log
ALTER COLUMN data DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForNotifications do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE notifications
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN seen SET NOT NULL")
end
def down do
execute("ALTER TABLE notifications
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN seen DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForOauthAuthorizations do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE oauth_authorizations
ALTER COLUMN app_id SET NOT NULL,
ALTER COLUMN token SET NOT NULL,
ALTER COLUMN used SET NOT NULL")
end
def down do
execute("ALTER TABLE oauth_authorizations
ALTER COLUMN app_id DROP NOT NULL,
ALTER COLUMN token DROP NOT NULL,
ALTER COLUMN used DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForOauthTokens do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE oauth_tokens
ALTER COLUMN app_id SET NOT NULL")
end
def down do
execute("ALTER TABLE oauth_tokens
ALTER COLUMN app_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForObjects do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE objects
ALTER COLUMN data SET NOT NULL")
end
def down do
execute("ALTER TABLE objects
ALTER COLUMN data DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForPasswordResetTokens do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE password_reset_tokens
ALTER COLUMN token SET NOT NULL,
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN used SET NOT NULL")
end
def down do
execute("ALTER TABLE password_reset_tokens
ALTER COLUMN token DROP NOT NULL,
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN used DROP NOT NULL")
end
end

View File

@ -0,0 +1,25 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForPushSubscriptions do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE push_subscriptions
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN token_id SET NOT NULL,
ALTER COLUMN endpoint SET NOT NULL,
ALTER COLUMN key_p256dh SET NOT NULL,
ALTER COLUMN key_auth SET NOT NULL,
ALTER COLUMN data SET NOT NULL")
end
def down do
execute("ALTER TABLE push_subscriptions
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN token_id DROP NOT NULL,
ALTER COLUMN endpoint DROP NOT NULL,
ALTER COLUMN key_p256dh DROP NOT NULL,
ALTER COLUMN key_auth DROP NOT NULL,
ALTER COLUMN data DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForRegistrations do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE registrations
ALTER COLUMN provider SET NOT NULL,
ALTER COLUMN uid SET NOT NULL,
ALTER COLUMN info SET NOT NULL")
end
def down do
execute("ALTER TABLE registrations
ALTER COLUMN provider DROP NOT NULL,
ALTER COLUMN uid DROP NOT NULL,
ALTER COLUMN info DROP NOT NULL")
end
end

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForScheduledActivities do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE scheduled_activities
ALTER COLUMN user_id SET NOT NULL")
end
def down do
execute("ALTER TABLE scheduled_activities
ALTER COLUMN user_id DROP NOT NULL")
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForThreadMutes do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE thread_mutes
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN context SET NOT NULL")
end
def down do
execute("ALTER TABLE thread_mutes
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN context DROP NOT NULL")
end
end

View File

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForUserInviteTokens do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
execute("ALTER TABLE user_invite_tokens
ALTER COLUMN used SET NOT NULL,
ALTER COLUMN uses SET NOT NULL,
ALTER COLUMN invite_type SET NOT NULL")
end
def down do
execute("ALTER TABLE user_invite_tokens
ALTER COLUMN used DROP NOT NULL,
ALTER COLUMN uses DROP NOT NULL,
ALTER COLUMN invite_type DROP NOT NULL")
end
end

View File

@ -0,0 +1,44 @@
defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do
use Ecto.Migration
# modify/3 function will require index recreation, so using execute/1 instead
def up do
# irreversible
execute("UPDATE users SET follower_count = 0 WHERE follower_count IS NULL")
execute("ALTER TABLE users
ALTER COLUMN local SET NOT NULL,
ALTER COLUMN source_data SET NOT NULL,
ALTER COLUMN note_count SET NOT NULL,
ALTER COLUMN follower_count SET NOT NULL,
ALTER COLUMN blocks SET NOT NULL,
ALTER COLUMN domain_blocks SET NOT NULL,
ALTER COLUMN mutes SET NOT NULL,
ALTER COLUMN muted_reblogs SET NOT NULL,
ALTER COLUMN muted_notifications SET NOT NULL,
ALTER COLUMN subscribers SET NOT NULL,
ALTER COLUMN pinned_activities SET NOT NULL,
ALTER COLUMN emoji SET NOT NULL,
ALTER COLUMN fields SET NOT NULL,
ALTER COLUMN raw_fields SET NOT NULL")
end
def down do
execute("ALTER TABLE users
ALTER COLUMN local DROP NOT NULL,
ALTER COLUMN source_data DROP NOT NULL,
ALTER COLUMN note_count DROP NOT NULL,
ALTER COLUMN follower_count DROP NOT NULL,
ALTER COLUMN blocks DROP NOT NULL,
ALTER COLUMN domain_blocks DROP NOT NULL,
ALTER COLUMN mutes DROP NOT NULL,
ALTER COLUMN muted_reblogs DROP NOT NULL,
ALTER COLUMN muted_notifications DROP NOT NULL,
ALTER COLUMN subscribers DROP NOT NULL,
ALTER COLUMN pinned_activities DROP NOT NULL,
ALTER COLUMN emoji DROP NOT NULL,
ALTER COLUMN fields DROP NOT NULL,
ALTER COLUMN raw_fields DROP NOT NULL")
end
end

View File

@ -0,0 +1,35 @@
defmodule Pleroma.Repo.Migrations.MigrateMissingFollowingRelationships do
use Ecto.Migration
def change do
execute(import_pending_follows_from_activities(), "")
end
defp import_pending_follows_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' = 'pending'
ORDER BY activities.updated_at DESC
ON CONFLICT DO NOTHING
"""
end
end

View File

@ -140,11 +140,15 @@ else
FULL_ARGS="$*" FULL_ARGS="$*"
ACTION="$1" ACTION="$1"
shift if [ $# -gt 0 ]; then
echo "$1" | grep "^-" >/dev/null shift
fi
echo "$1" | grep "^-" >/dev/null
if [ $? -eq 1 ]; then if [ $? -eq 1 ]; then
SUBACTION="$1" SUBACTION="$1"
shift if [ $# -gt 0 ]; then
shift
fi
fi fi
if [ "$ACTION" = "update" ]; then if [ "$ACTION" = "update" ]; then

View File

@ -140,7 +140,7 @@ test "it marks all the user's participations as read" do
participation2 = insert(:participation, %{read: false, user: user}) participation2 = insert(:participation, %{read: false, user: user})
participation3 = insert(:participation, %{read: false, user: other_user}) participation3 = insert(:participation, %{read: false, user: other_user})
{:ok, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user) {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
assert Participation.get(participation1.id).read == true assert Participation.get(participation1.id).read == true
assert Participation.get(participation2.id).read == true assert Participation.get(participation2.id).read == true
@ -216,4 +216,134 @@ test "it sets recipients, always keeping the owner of the participation even whe
assert user in participation.recipients assert user in participation.recipients
assert other_user in participation.recipients assert other_user in participation.recipients
end end
describe "blocking" do
test "when the user blocks a recipient, the existing conversations with them are marked as read" do
blocker = insert(:user)
blocked = insert(:user)
third_user = insert(:user)
{:ok, _direct1} =
CommonAPI.post(third_user, %{
"status" => "Hi @#{blocker.nickname}",
"visibility" => "direct"
})
{:ok, _direct2} =
CommonAPI.post(third_user, %{
"status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
"visibility" => "direct"
})
{:ok, _direct3} =
CommonAPI.post(blocked, %{
"status" => "Hi @#{blocker.nickname}",
"visibility" => "direct"
})
{:ok, _direct4} =
CommonAPI.post(blocked, %{
"status" => "Hi @#{blocker.nickname}, @#{third_user.nickname}",
"visibility" => "direct"
})
assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
{:ok, blocker} = User.block(blocker, blocked)
# The conversations with the blocked user are marked as read
assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1
# The conversation is not marked as read for the blocked user
assert [_, _, %{read: false}] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
# The conversation is not marked as read for the third user
assert [%{read: false}, _, _] = Participation.for_user(third_user)
assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1
end
test "the new conversation with the blocked user is not marked as unread " do
blocker = insert(:user)
blocked = insert(:user)
third_user = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
# When the blocked user is the author
{:ok, _direct1} =
CommonAPI.post(blocked, %{
"status" => "Hi @#{blocker.nickname}",
"visibility" => "direct"
})
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
# When the blocked user is a recipient
{:ok, _direct2} =
CommonAPI.post(third_user, %{
"status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
"visibility" => "direct"
})
assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
assert [%{read: false}, _] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
end
test "the conversation with the blocked user is not marked as unread on a reply" do
blocker = insert(:user)
blocked = insert(:user)
third_user = insert(:user)
{:ok, _direct1} =
CommonAPI.post(blocker, %{
"status" => "Hi @#{third_user.nickname}, @#{blocked.nickname}",
"visibility" => "direct"
})
{:ok, blocker} = User.block(blocker, blocked)
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
assert [blocked_participation] = Participation.for_user(blocked)
# When it's a reply from the blocked user
{:ok, _direct2} =
CommonAPI.post(blocked, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_conversation_id" => blocked_participation.id
})
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
assert [third_user_participation] = Participation.for_user(third_user)
# When it's a reply from the third user
{:ok, _direct3} =
CommonAPI.post(third_user, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_conversation_id" => third_user_participation.id
})
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
# Marked as unread for the blocked user
assert [%{read: false}] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
end
end
end end

View File

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

View File

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

View File

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

View File

@ -139,7 +139,8 @@ test "no user to toggle" do
describe "running unsubscribe" do describe "running unsubscribe" do
test "user is unsubscribed" do test "user is unsubscribed" do
followed = insert(:user) 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]) Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
@ -154,7 +155,7 @@ test "user is unsubscribed" do
assert message =~ "Successfully unsubscribed" assert message =~ "Successfully unsubscribed"
user = User.get_cached_by_nickname(user.nickname) user = User.get_cached_by_nickname(user.nickname)
assert Enum.empty?(user.following) assert Enum.empty?(User.get_friends(user))
assert user.deactivated assert user.deactivated
end 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(pending_follower, locked) CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_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 [^pending_follower] = User.get_follow_requests(locked)
assert activity
end end
test "clears follow requests when requester is blocked" do 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) followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one]) {: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]) {:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert length(user.following) == 4 assert length(User.following(user)) == 4
end end
test "follow takes a user and another user" do 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) followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.follower_count == 1 assert followed.follower_count == 1
assert User.ap_followers(followed) in user.following assert User.ap_followers(followed) in User.following(user)
end end
test "can't follow a deactivated users" do test "can't follow a deactivated users" do
@ -218,26 +217,29 @@ test "unfollow with syncronizes external user" do
nickname: "fuser2", nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2", ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers", follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following", following_address: "http://localhost:4001/users/fuser2/following"
following: [User.ap_followers(followed)]
}) })
{:ok, user} = User.follow(user, followed, "accept")
{:ok, user, _activity} = User.unfollow(user, followed) {:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
assert user.following == [] assert User.following(user) == []
end end
test "unfollow takes a user and another user" do test "unfollow takes a user and another user" do
followed = insert(:user) 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) {:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id) assert User.following(user) == [user.follower_address]
assert user.following == []
end end
test "unfollow doesn't unfollow yourself" do test "unfollow doesn't unfollow yourself" do
@ -245,14 +247,14 @@ test "unfollow doesn't unfollow yourself" do
{:error, _} = User.unfollow(user, user) {:error, _} = User.unfollow(user, user)
user = User.get_cached_by_id(user.id) assert User.following(user) == [user.follower_address]
assert user.following == [user.ap_id]
end end
end end
test "test if a user is following another user" do test "test if a user is following another user" do
followed = insert(:user) followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]}) user = insert(:user)
User.follow(user, followed, "accept")
assert User.following?(user, followed) assert User.following?(user, followed)
refute User.following?(followed, user) refute User.following?(followed, user)
@ -335,7 +337,7 @@ test "it restricts certain nicknames" do
refute changeset.valid? refute changeset.valid?
end 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) changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid? 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 is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) 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" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
@ -650,37 +648,6 @@ test "it sets the follower_count property" do
end end
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 describe "follow_import" do
test "it imports user followings from list" do test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user) [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] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == 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) {: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 [] == Pleroma.Notification.for_user(user2)
assert [] == assert [] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
"user" => user2
})
end end
end end

View File

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

View File

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

View File

@ -720,7 +720,7 @@ test "it fails for incoming deletes with spoofed origin" do
assert capture_log(fn -> assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data) :error = Transmogrifier.handle_incoming(data)
end) =~ end) =~
"[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}" "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id) assert Activity.get_by_id(activity.id)
end end
@ -804,6 +804,25 @@ test "it works for incomming unfollows with an existing follow" do
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end end
test "it works for incoming follows to locked account" do
pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
user = insert(:user, locked: true)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Follow"
assert data["object"] == user.ap_id
assert data["state"] == "pending"
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert [^pending_follower] = User.get_follow_requests(user)
end
test "it works for incoming blocks" do test "it works for incoming blocks" do
user = insert(:user) user = insert(:user)
@ -1334,7 +1353,8 @@ test "it upgrades a user to activitypub" do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) 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, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"}) {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
@ -1381,8 +1401,8 @@ test "it upgrades a user to activitypub" do
refute user.follower_address in unrelated_activity.recipients refute user.follower_address in unrelated_activity.recipients
user_two = User.get_cached_by_id(user_two.id) user_two = User.get_cached_by_id(user_two.id)
assert user.follower_address in user_two.following assert User.following?(user_two, user)
refute "..." in user_two.following refute "..." in User.following(user_two)
end end
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 test "returns true if user following to author" do
author = insert(:user) author = insert(:user)
user = insert(:user, following: [author.ap_id]) user = insert(:user)
Pleroma.User.follow(user, author)
activity = activity =
insert(:note_activity, insert(:note_activity,

View File

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

View File

@ -471,7 +471,7 @@ test "following without reblogs" do
conn = conn =
build_conn() build_conn()
|> assign(:user, follower) |> assign(:user, User.get_cached_by_id(follower.id))
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true") |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
assert %{"showing_reblogs" => true} = json_response(conn, 200) 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) other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, 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)
assert User.following?(other_user, user) == false assert User.following?(other_user, user) == false
@ -36,6 +34,7 @@ test "/api/v1/follow_requests/:id/authorize works" do
other_user = insert(:user) other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, 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) user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_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 test "it doesn't send to user if recipients invalid and thread containment is enabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false) Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user) author = insert(:user)
user = insert(:user, following: [author.ap_id]) user = insert(:user)
User.follow(user, author, "accept")
activity = activity =
insert(:note_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 test "it sends message if recipients invalid and thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], true) Pleroma.Config.put([:instance, :skip_thread_containment], true)
author = insert(:user) author = insert(:user)
user = insert(:user, following: [author.ap_id]) user = insert(:user)
User.follow(user, author, "accept")
activity = activity =
insert(:note_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 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) Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user) 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 = activity =
insert(:note_activity, insert(:note_activity,

View File

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