|> select([r, u], u.follower_address)
|> Repo.all()
- if not user.local or user.nickname in [nil, "internal.fetch"] do
+ if not user.local or user.invisible do
following
else
[user.follower_address | following]
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
- # Should be filled in only for remote users
- field(:following_count, :integer, default: nil)
+ field(:following_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
def visible_for?(user, for_user \\ nil)
+ def visible_for?(%User{invisible: true}, _), do: false
+
def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
def visible_for?(%User{} = user, for_user) do
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def user_info(%User{} = user, args \\ %{}) do
- following_count =
- Map.get(args, :following_count, user.following_count || following_count(user))
-
+ following_count = Map.get(args, :following_count, user.following_count)
follower_count = Map.get(args, :follower_count, user.follower_count)
%{
note_count: user.note_count,
locked: user.locked,
confirmation_pending: user.confirmation_pending,
- default_scope: user.default_scope
+ default_scope: user.default_scope,
+ follower_count: follower_count,
+ following_count: following_count
}
- |> Map.put(:following_count, following_count)
- |> Map.put(:follower_count, follower_count)
end
def follow_state(%User{} = user, %User{} = target) 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
- followeds =
- Enum.reject(followeds, fn followed ->
- blocks?(follower, followed) || blocks?(followed, follower)
- end)
-
- Enum.each(followeds, &follow(follower, &1, "accept"))
-
- Enum.each(followeds, &update_follower_count/1)
+ followeds
+ |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+ |> Enum.each(&follow(follower, &1, "accept"))
set_cache(follower)
end
true ->
FollowingRelationship.follow(follower, followed, state)
- follower = maybe_update_following_count(follower)
-
{:ok, _} = update_follower_count(followed)
- set_cache(follower)
+ follower
+ |> update_following_count()
+ |> set_cache()
end
end
if following?(follower, followed) and follower.ap_id != followed.ap_id do
FollowingRelationship.unfollow(follower, followed)
- follower = maybe_update_following_count(follower)
-
{:ok, followed} = update_follower_count(followed)
- set_cache(follower)
+ {:ok, follower} =
+ follower
+ |> update_following_count()
+ |> set_cache()
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
end
end
- @spec maybe_update_following_count(User.t()) :: User.t()
- def maybe_update_following_count(%User{local: false} = user) do
+ @spec update_following_count(User.t()) :: User.t()
+ def update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
maybe_fetch_follow_information(user)
else
end
end
- def maybe_update_following_count(user), do: user
+ def update_following_count(%User{local: true} = user) do
+ following_count = FollowingRelationship.following_count(user)
+
+ user
+ |> follow_information_changeset(%{following_count: following_count})
+ |> Repo.update!()
+ end
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
def deactivate(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
- Enum.each(get_followers(user), &invalidate_cache/1)
+ user
+ |> get_followers()
+ |> Enum.filter(& &1.local)
+ |> Enum.each(fn follower ->
+ follower |> update_following_count() |> set_cache()
+ end)
# Only update local user counts, remote will be update during the next pull.
user
end
end
- @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
+ @doc """
+ Creates an internal service actor by URI if missing.
+ Optionally takes nickname for addressing.
+ """
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
- with %User{} = user <- get_cached_by_ap_id(uri) do
- user
- else
- _ ->
- {:ok, user} =
- %User{}
- |> cast(%{}, [:ap_id, :nickname, :local])
- |> put_change(:ap_id, uri)
- |> put_change(:nickname, nickname)
- |> put_change(:local, true)
- |> put_change(:follower_address, uri <> "/followers")
- |> Repo.insert()
+ with user when is_nil(user) <- get_cached_by_ap_id(uri) do
+ {:ok, user} =
+ %User{
+ invisible: true,
+ local: true,
+ ap_id: uri,
+ nickname: nickname,
+ follower_address: uri <> "/followers"
+ }
+ |> Repo.insert()
- user
+ user
end
end
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
+ |> filter_invisible_users()
|> filter_blocked_domains(for_user)
|> fts_search(query_string)
|> trigram_rank(query_string)
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
+ defp filter_invisible_users(query) do
+ from(q in query, where: q.invisible == false)
+ end
+
defp filter_blocked_user(query, %User{} = blocker) do
query
|> join(:left, [u], b in Pleroma.UserRelationship,
relay_ap_id()
|> User.get_or_create_service_actor_by_ap_id()
- {:ok, actor} = User.set_invisible(actor, true)
actor
end
@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
- true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
+ true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
_e -> render_error(conn, :not_found, "Can't find user")
identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen
- {git_tag, git_pre_release} =
+ git_pre_release =
with {tag, 0} <-
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
- tag = String.trim(tag),
- {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
- describe = String.trim(describe),
- ahead <- String.replace(describe, tag, ""),
- ahead <- String.trim_leading(ahead, "-") do
- {String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
+ {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
+ describe
+ |> String.trim()
+ |> String.replace(String.trim(tag), "")
+ |> String.trim_leading("-")
+ |> String.trim()
else
_ ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
- {nil, "0-g" <> String.trim(commit_hash)}
+ "0-g" <> String.trim(commit_hash)
end
- if git_tag && version != git_tag do
- Mix.shell().error(
- "Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
- )
- end
-
# Branch name as pre-release version component, denoted with a dot
branch_name =
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetVisibleServiceActors do
+ use Ecto.Migration
+ import Ecto.Query
+ alias Pleroma.Repo
+
+ def up do
+ user_nicknames = ["relay", "internal.fetch"]
+
+ from(
+ u in "users",
+ where: u.nickname in ^user_nicknames,
+ update: [
+ set: [invisible: true]
+ ]
+ )
+ |> Repo.update_all([])
+ end
+
+ def down do
+ :ok
+ end
+end
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FollowingRelationshipTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.Web.ActivityPub.InternalFetchActor
+ alias Pleroma.Web.ActivityPub.Relay
+
+ import Pleroma.Factory
+
+ describe "following/1" do
+ test "returns following addresses without internal.fetch" do
+ user = insert(:user)
+ fetch_actor = InternalFetchActor.get_actor()
+ FollowingRelationship.follow(fetch_actor, user, "accept")
+ assert FollowingRelationship.following(fetch_actor) == [user.follower_address]
+ end
+
+ test "returns following addresses without relay" do
+ user = insert(:user)
+ relay_actor = Relay.get_actor()
+ FollowingRelationship.follow(relay_actor, user, "accept")
+ assert FollowingRelationship.following(relay_actor) == [user.follower_address]
+ end
+
+ test "returns following addresses without remote user" do
+ user = insert(:user)
+ actor = insert(:user, local: false)
+ FollowingRelationship.follow(actor, user, "accept")
+ assert FollowingRelationship.following(actor) == [user.follower_address]
+ end
+
+ test "returns following addresses with local user" do
+ user = insert(:user)
+ actor = insert(:user, local: true)
+ FollowingRelationship.follow(actor, user, "accept")
+
+ assert FollowingRelationship.following(actor) == [
+ actor.follower_address,
+ user.follower_address
+ ]
+ end
+ end
+end
end
describe "User.search" do
+ test "excluded invisible users from results" do
+ user = insert(:user, %{nickname: "john t1000"})
+ insert(:user, %{invisible: true, nickname: "john t800"})
+
+ [found_user] = User.search("john")
+ assert found_user.id == user.id
+ end
+
test "accepts limit parameter" do
Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
assert length(User.search("john", limit: 3)) == 3
clear_config([:instance, :account_activation_required])
+ describe "service actors" do
+ test "returns invisible actor" do
+ uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
+ followers_uri = "#{uri}/followers"
+ user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+
+ assert %User{
+ nickname: "internal.fetch-test",
+ invisible: true,
+ local: true,
+ ap_id: ^uri,
+ follower_address: ^followers_uri
+ } = user
+
+ user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+ assert user.id == user2.id
+ end
+ end
+
describe "AP ID user relationships" do
setup do
{:ok, user: insert(:user)}
{:ok, user} = User.follow(user, followed)
user = User.get_cached_by_id(user.id)
-
followed = User.get_cached_by_ap_id(followed.ap_id)
+
assert followed.follower_count == 1
+ assert user.following_count == 1
assert User.ap_followers(followed) in User.following(user)
end
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
+ assert user2.following_count == 1
assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2)
+ assert refresh_record(user2).following_count == 0
assert info.following_count == 0
assert User.following_count(user2) == 0
assert [] = User.get_friends(user2)
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token
refute acc_one == acc_two
assert acc_two == acc_three
end
+
+ test "returns 404 when user is invisible", %{conn: conn} do
+ user = insert(:user, %{invisible: true})
+
+ resp =
+ conn
+ |> get("/api/v1/accounts/#{user.nickname}")
+ |> json_response(404)
+
+ assert %{"error" => "Can't find user"} = resp
+ end
+
+ test "returns 404 for internal.fetch actor", %{conn: conn} do
+ %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
+
+ resp =
+ conn
+ |> get("/api/v1/accounts/internal.fetch")
+ |> json_response(404)
+
+ assert %{"error" => "Can't find user"} = resp
+ end
end
describe "user timelines" do