X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fuser.ex;h=974f9685253f2cb7a43c803a3f30a8c335483e12;hb=325372b1e1eba6d9a2468678b5bf17c88252b61d;hp=474cd8c1a1b668026271290b253713b321bdaf6b;hpb=f4e2595592ccca6cedd64669baef7bdd2a6547d0;p=akkoma diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 474cd8c1a..974f96852 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -9,12 +9,14 @@ defmodule Pleroma.User do import Ecto.Query alias Comeonin.Pbkdf2 + alias Ecto.Multi alias Pleroma.Activity alias Pleroma.Keys alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Registration alias Pleroma.Repo + alias Pleroma.RepoStreamer alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub @@ -50,6 +52,7 @@ defmodule Pleroma.User do field(:avatar, :map) field(:local, :boolean, default: true) field(:follower_address, :string) + field(:following_address, :string) field(:search_rank, :float, virtual: true) field(:search_type, :integer, virtual: true) field(:tags, {:array, :string}, default: []) @@ -105,17 +108,34 @@ defmodule Pleroma.User do def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" - def user_info(%User{} = user) do + @spec ap_following(User.t()) :: Sring.t() + def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa + def ap_following(%User{} = user), do: "#{ap_id(user)}/following" + + def user_info(%User{} = user, args \\ %{}) do + following_count = + if args[:following_count], + do: args[:following_count], + else: user.info.following_count || following_count(user) + + follower_count = + if args[:follower_count], do: args[:follower_count], else: user.info.follower_count + %{ - following_count: following_count(user), note_count: user.info.note_count, - follower_count: user.info.follower_count, locked: user.info.locked, confirmation_pending: user.info.confirmation_pending, default_scope: user.info.default_scope } + |> Map.put(:following_count, following_count) + |> Map.put(:follower_count, follower_count) + end + + def set_info_cache(user, args) do + Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args)) end + @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() def restrict_deactivated(query) do from(u in query, where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) @@ -150,9 +170,10 @@ defmodule Pleroma.User do if changes.valid? do case info_cng.changes[:source_data] do - %{"followers" => followers} -> + %{"followers" => followers, "following" => following} -> changes |> put_change(:follower_address, followers) + |> put_change(:following_address, following) _ -> followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) @@ -184,7 +205,14 @@ defmodule Pleroma.User do |> User.Info.user_upgrade(params[:info]) struct - |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at]) + |> cast(params, [ + :bio, + :name, + :follower_address, + :following_address, + :avatar, + :last_refreshed_at + ]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) @@ -193,29 +221,27 @@ defmodule Pleroma.User do end def password_update_changeset(struct, params) do - changeset = - struct - |> cast(params, [:password, :password_confirmation]) - |> validate_required([:password, :password_confirmation]) - |> validate_confirmation(:password) - - OAuth.Token.delete_user_tokens(struct) - OAuth.Authorization.delete_user_authorizations(struct) - - if changeset.valid? do - hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) - - changeset - |> put_change(:password_hash, hashed) - else - changeset + struct + |> cast(params, [:password, :password_confirmation]) + |> validate_required([:password, :password_confirmation]) + |> validate_confirmation(:password) + |> put_password_hash + end + + @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} + def reset_password(%User{id: user_id} = user, data) do + multi = + Multi.new() + |> Multi.update(:user, password_update_changeset(user, data)) + |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) + |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) + + case Repo.transaction(multi) do + {:ok, %{user: user} = _} -> set_cache(user) + {:error, _, changeset, _} -> {:error, changeset} end end - def reset_password(user, data) do - update_and_set_cache(password_update_changeset(user, data)) - end - def register_changeset(struct, params \\ %{}, opts \\ []) do need_confirmation? = if is_nil(opts[:need_confirmation]) do @@ -249,12 +275,11 @@ defmodule Pleroma.User do end if changeset.valid? do - hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]}) changeset - |> put_change(:password_hash, hashed) + |> put_password_hash |> put_change(:ap_id, ap_id) |> unique_constraint(:ap_id) |> put_change(:following, [followers]) @@ -308,6 +333,7 @@ defmodule Pleroma.User do def needs_update?(_), do: true + @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do {:ok, follower} end @@ -324,14 +350,6 @@ defmodule Pleroma.User do end end - def maybe_follow(%User{} = follower, %User{info: _info} = followed) do - if not following?(follower, followed) do - follow(follower, followed) - else - {:ok, follower} - end - end - @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 @@ -370,8 +388,8 @@ defmodule Pleroma.User do ap_followers = followed.follower_address cond do - following?(follower, followed) or info.deactivated -> - {:error, "Could not follow user: #{followed.nickname} is already on your list."} + info.deactivated -> + {:error, "Could not follow user: You are deactivated."} deny_follow_blocked and blocks?(followed, follower) -> {:error, "Could not follow user: #{followed.nickname} blocked you."} @@ -390,6 +408,8 @@ defmodule Pleroma.User do {1, [follower]} = Repo.update_all(q, []) + follower = maybe_update_following_count(follower) + {:ok, _} = update_follower_count(followed) set_cache(follower) @@ -409,6 +429,8 @@ defmodule Pleroma.User do {1, [follower]} = Repo.update_all(q, []) + follower = maybe_update_following_count(follower) + {:ok, followed} = update_follower_count(followed) set_cache(follower) @@ -457,7 +479,7 @@ defmodule Pleroma.User do end def update_and_set_cache(changeset) do - with {:ok, user} <- Repo.update(changeset) do + with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do set_cache(user) else e -> e @@ -572,12 +594,23 @@ defmodule Pleroma.User do @spec get_followers_query(User.t()) :: Ecto.Query.t() def get_followers_query(user), do: get_followers_query(user, nil) + @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())} def get_followers(user, page \\ nil) do q = get_followers_query(user, page) {:ok, Repo.all(q)} end + @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())} + def get_external_followers(user, page \\ nil) do + q = + user + |> get_followers_query(page) + |> User.Query.build(%{external: true}) + + {:ok, Repo.all(q)} + end + def get_followers_ids(user, page \\ nil) do q = get_followers_query(user, page) @@ -682,32 +715,73 @@ defmodule Pleroma.User do |> update_and_set_cache() end + def maybe_fetch_follow_information(user) do + with {:ok, user} <- fetch_follow_information(user) do + user + else + e -> + Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}") + + user + end + end + + def fetch_follow_information(user) do + with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do + info_cng = User.Info.follow_information_update(user.info, info) + + changeset = + user + |> change() + |> put_embed(:info, info_cng) + + update_and_set_cache(changeset) + else + {:error, _} = e -> e + e -> {:error, e} + end + end + def update_follower_count(%User{} = user) do - follower_count_query = - User.Query.build(%{followers: user, deactivated: false}) - |> select([u], %{count: count(u.id)}) + if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do + follower_count_query = + User.Query.build(%{followers: user, deactivated: false}) + |> select([u], %{count: count(u.id)}) + + User + |> where(id: ^user.id) + |> join(:inner, [u], s in subquery(follower_count_query)) + |> update([u, s], + set: [ + info: + fragment( + "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)", + u.info, + s.count + ) + ] + ) + |> select([u], u) + |> Repo.update_all([]) + |> case do + {1, [user]} -> set_cache(user) + _ -> {:error, user} + end + else + {:ok, maybe_fetch_follow_information(user)} + end + end - User - |> where(id: ^user.id) - |> join(:inner, [u], s in subquery(follower_count_query)) - |> update([u, s], - set: [ - info: - fragment( - "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)", - u.info, - s.count - ) - ] - ) - |> select([u], u) - |> Repo.update_all([]) - |> case do - {1, [user]} -> set_cache(user) - _ -> {:error, user} + def maybe_update_following_count(%User{local: false} = user) do + if Pleroma.Config.get([:instance, :external_user_synchronization]) do + {:ok, maybe_fetch_follow_information(user)} + else + user end end + def maybe_update_following_count(user), do: user + def remove_duplicated_following(%User{following: following} = user) do uniq_following = Enum.uniq(following) @@ -735,126 +809,13 @@ defmodule Pleroma.User do |> Repo.all() end - def search(query, resolve \\ false, for_user \\ nil) do - # Strip the beginning @ off if there is a query - query = String.trim_leading(query, "@") - - if resolve, do: get_or_fetch(query) - - {:ok, results} = - Repo.transaction(fn -> - Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) - Repo.all(search_query(query, for_user)) - end) - - results - end - - def search_query(query, for_user) do - fts_subquery = fts_search_subquery(query) - trigram_subquery = trigram_search_subquery(query) - union_query = from(s in trigram_subquery, union_all: ^fts_subquery) - distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id) - - from(s in subquery(boost_search_rank_query(distinct_query, for_user)), - order_by: [desc: s.search_rank], - limit: 40 - ) - end - - defp boost_search_rank_query(query, nil), do: query - - defp boost_search_rank_query(query, for_user) do - friends_ids = get_friends_ids(for_user) - followers_ids = get_followers_ids(for_user) + @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} + def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do + info = muter.info - from(u in subquery(query), - select_merge: %{ - search_rank: - fragment( - """ - CASE WHEN (?) THEN (?) * 1.3 - WHEN (?) THEN (?) * 1.2 - WHEN (?) THEN (?) * 1.1 - ELSE (?) END - """, - u.id in ^friends_ids and u.id in ^followers_ids, - u.search_rank, - u.id in ^friends_ids, - u.search_rank, - u.id in ^followers_ids, - u.search_rank, - u.search_rank - ) - } - ) - end - - defp fts_search_subquery(term, query \\ User) do - processed_query = - term - |> String.replace(~r/\W+/, " ") - |> String.trim() - |> String.split() - |> Enum.map(&(&1 <> ":*")) - |> Enum.join(" | ") - - from( - u in query, - select_merge: %{ - search_type: ^0, - search_rank: - fragment( - """ - ts_rank_cd( - setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'), - to_tsquery('simple', ?), - 32 - ) - """, - u.nickname, - u.name, - ^processed_query - ) - }, - where: - fragment( - """ - (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?) - """, - u.nickname, - u.name, - ^processed_query - ) - ) - |> restrict_deactivated() - end - - defp trigram_search_subquery(term) do - from( - u in User, - select_merge: %{ - # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason - search_type: fragment("?", 1), - search_rank: - fragment( - "similarity(?, trim(? || ' ' || coalesce(?, '')))", - ^term, - u.nickname, - u.name - ) - }, - where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) - ) - |> restrict_deactivated() - end - - def mute(muter, %User{ap_id: ap_id}) do info_cng = - muter.info - |> User.Info.add_to_mutes(ap_id) + User.Info.add_to_mutes(info, ap_id) + |> User.Info.add_to_muted_notifications(info, ap_id, notifications?) cng = change(muter) @@ -864,9 +825,11 @@ defmodule Pleroma.User do end def unmute(muter, %{ap_id: ap_id}) do + info = muter.info + info_cng = - muter.info - |> User.Info.remove_from_mutes(ap_id) + User.Info.remove_from_mutes(info, ap_id) + |> User.Info.remove_from_muted_notifications(info, ap_id) cng = change(muter) @@ -962,17 +925,32 @@ defmodule Pleroma.User do def mutes?(nil, _), do: false def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id) - def blocks?(user, %{ap_id: ap_id}) do - blocks = user.info.blocks - domain_blocks = user.info.domain_blocks - %{host: host} = URI.parse(ap_id) + @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() + def muted_notifications?(nil, _), do: false - Enum.member?(blocks, ap_id) || - Enum.any?(domain_blocks, fn domain -> - host == domain - end) + def muted_notifications?(user, %{ap_id: ap_id}), + do: Enum.member?(user.info.muted_notifications, ap_id) + + def blocks?(%User{} = user, %User{} = target) do + blocks_ap_id?(user, target) || blocks_domain?(user, target) + end + + def blocks?(nil, _), do: false + + def blocks_ap_id?(%User{} = user, %User{} = target) do + Enum.member?(user.info.blocks, target.ap_id) + end + + def blocks_ap_id?(_, _), do: false + + def blocks_domain?(%User{} = user, %User{} = target) do + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) + %{host: host} = URI.parse(target.ap_id) + Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) end + def blocks_domain?(_, _), do: false + def subscribed_to?(user, %{ap_id: ap_id}) do with %User{} = target <- get_cached_by_ap_id(ap_id) do Enum.member?(target.info.subscribers, user.ap_id) @@ -1056,18 +1034,26 @@ defmodule Pleroma.User do @spec perform(atom(), User.t()) :: {:ok, User.t()} def perform(:delete, %User{} = user) do - {:ok, user} = User.deactivate(user) + {:ok, _user} = ActivityPub.delete(user) # Remove all relationships {:ok, followers} = User.get_followers(user) - Enum.each(followers, fn follower -> User.unfollow(follower, user) end) + Enum.each(followers, fn follower -> + ActivityPub.unfollow(follower, user) + User.unfollow(follower, user) + end) {:ok, friends} = User.get_friends(user) - Enum.each(friends, fn followed -> User.unfollow(user, followed) end) + Enum.each(friends, fn followed -> + ActivityPub.unfollow(user, followed) + User.unfollow(user, followed) + end) delete_user_activities(user) + invalidate_cache(user) + Repo.delete(user) end @spec perform(atom(), User.t()) :: {:ok, User.t()} @@ -1123,6 +1109,34 @@ defmodule Pleroma.User do ) end + @spec external_users_query() :: Ecto.Query.t() + def external_users_query do + User.Query.build(%{ + external: true, + active: true, + order_by: :id + }) + end + + @spec external_users(keyword()) :: [User.t()] + def external_users(opts \\ []) do + query = + external_users_query() + |> select([u], struct(u, [:id, :ap_id, :info])) + + query = + if opts[:max_id], + do: where(query, [u], u.id > ^opts[:max_id]), + else: query + + query = + if opts[:limit], + do: limit(query, ^opts[:limit]), + else: query + + Repo.all(query) + end + def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers), do: PleromaJobQueue.enqueue(:background, __MODULE__, [ @@ -1140,18 +1154,35 @@ defmodule Pleroma.User do ]) def delete_user_activities(%User{ap_id: ap_id} = user) do - stream = - ap_id - |> Activity.query_by_actor() - |> Repo.stream() - - Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) + ap_id + |> Activity.query_by_actor() + |> RepoStreamer.chunk_stream(50) + |> Stream.each(fn activities -> + Enum.each(activities, &delete_activity(&1)) + end) + |> Stream.run() {:ok, user} end defp delete_activity(%{data: %{"type" => "Create"}} = activity) do - Object.normalize(activity) |> ActivityPub.delete() + activity + |> Object.normalize() + |> ActivityPub.delete() + end + + defp delete_activity(%{data: %{"type" => "Like"}} = activity) do + user = get_cached_by_ap_id(activity.actor) + object = Object.normalize(activity) + + ActivityPub.unlike(user, object) + end + + defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do + user = get_cached_by_ap_id(activity.actor) + object = Object.normalize(activity) + + ActivityPub.unannounce(user, object) end defp delete_activity(_activity), do: "Doing nothing" @@ -1160,9 +1191,7 @@ defmodule Pleroma.User do Pleroma.HTML.Scrubber.TwitterText end - @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy]) - - def html_filter_policy(_), do: @default_scrubbers + def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) def fetch_by_ap_id(ap_id) do ap_try = ActivityPub.make_user_from_ap_id(ap_id) @@ -1200,19 +1229,18 @@ defmodule Pleroma.User do end end - def get_or_create_instance_user do - relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" - - if user = get_cached_by_ap_id(relay_uri) do + @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 + if user = get_cached_by_ap_id(uri) do user else changes = %User{info: %User.Info{}} |> cast(%{}, [:ap_id, :nickname, :local]) - |> put_change(:ap_id, relay_uri) - |> put_change(:nickname, nil) + |> put_change(:ap_id, uri) + |> put_change(:nickname, nickname) |> put_change(:local, true) - |> put_change(:follower_address, relay_uri <> "/followers") + |> put_change(:follower_address, uri <> "/followers") {:ok, user} = Repo.insert(changes) user @@ -1233,10 +1261,12 @@ defmodule Pleroma.User do end # OStatus Magic Key - def public_key_from_info(%{magic_key: magic_key}) do + def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do {:ok, Pleroma.Web.Salmon.decode_key(magic_key)} end + def public_key_from_info(_), do: {:error, "not found key"} + def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), {:ok, public_key} <- public_key_from_info(user.info) do @@ -1253,7 +1283,7 @@ defmodule Pleroma.User do data |> Map.put(:name, blank?(data[:name]) || data[:nickname]) |> remote_user_creation() - |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname) + |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname) |> set_cache() end @@ -1422,23 +1452,38 @@ defmodule Pleroma.User do } end - def ensure_keys_present(user) do - info = user.info - + def ensure_keys_present(%User{info: info} = user) do if info.keys do {:ok, user} else {:ok, pem} = Keys.generate_rsa_pem() - info_cng = - info - |> User.Info.set_keys(pem) + user + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem)) + |> update_and_set_cache() + end + end + + def get_ap_ids_by_nicknames(nicknames) do + from(u in User, + where: u.nickname in ^nicknames, + select: u.ap_id + ) + |> Repo.all() + end - cng = - Ecto.Changeset.change(user) - |> Ecto.Changeset.put_embed(:info, info_cng) + defdelegate search(query, opts \\ []), to: User.Search - update_and_set_cache(cng) - end + defp put_password_hash( + %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset + ) do + change(changeset, password_hash: Pbkdf2.hashpwsalt(password)) end + + defp put_password_hash(changeset), do: changeset + + def is_internal_user?(%User{nickname: nil}), do: true + def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true + def is_internal_user?(_), do: false end