X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fuser.ex;h=09f86aaa2d0a840ddaf57b757249356b1204ef14;hb=4948117fcf297efa735cdafb9cddf39946668266;hp=cf378d46772d73f41bee44e5dcbb3547e3f488a1;hpb=5e2b491276d5cd8d90fddf219f7653d1c9b31ef3;p=akkoma diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index cf378d467..09f86aaa2 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -9,11 +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 @@ -55,7 +58,7 @@ defmodule Pleroma.User do field(:last_refreshed_at, :naive_datetime_usec) has_many(:notifications, Notification) has_many(:registrations, Registration) - embeds_one(:info, Pleroma.User.Info) + embeds_one(:info, User.Info) timestamps() end @@ -115,7 +118,7 @@ defmodule Pleroma.User do } end - defp restrict_deactivated(query) do + def restrict_deactivated(query) do from(u in query, where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) ) @@ -166,7 +169,7 @@ defmodule Pleroma.User do def update_changeset(struct, params \\ %{}) do struct - |> cast(params, [:bio, :name, :avatar]) + |> cast(params, [:bio, :name, :avatar, :following]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) @@ -192,29 +195,26 @@ 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 + + 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 @@ -233,7 +233,7 @@ defmodule Pleroma.User do |> validate_confirmation(:password) |> unique_constraint(:email) |> unique_constraint(:nickname) - |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) + |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) |> validate_length(:bio, max: 1000) @@ -248,12 +248,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]) @@ -278,7 +277,7 @@ defmodule Pleroma.User do with {:ok, user} <- Repo.insert(changeset), {:ok, user} <- autofollow_users(user), {:ok, user} <- set_cache(user), - {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), + {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- try_send_confirmation_email(user) do {:ok, user} end @@ -323,14 +322,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 @@ -365,14 +356,12 @@ defmodule Pleroma.User do end def follow(%User{} = follower, %User{info: info} = followed) do - user_config = Application.get_env(:pleroma, :user) - deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked) - + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) 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."} @@ -425,24 +414,6 @@ defmodule Pleroma.User do Enum.member?(follower.following, followed.follower_address) end - def follow_import(%User{} = follower, followed_identifiers) - when is_list(followed_identifiers) do - Enum.map( - followed_identifiers, - fn followed_identifier -> - with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), - {:ok, follower} <- maybe_direct_follow(follower, followed), - {:ok, _} <- ActivityPub.follow(follower, followed) do - followed - else - err -> - Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}") - err - end - end - ) - end - def locked?(%User{} = user) do user.info.locked || false end @@ -564,8 +535,7 @@ defmodule Pleroma.User do with [_nick, _domain] <- String.split(nickname, "@"), {:ok, user} <- fetch_by_nickname(nickname) do if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do - # TODO turn into job - {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) + fetch_initial_posts(user) end {:ok, user} @@ -576,15 +546,8 @@ defmodule Pleroma.User do end @doc "Fetch some posts when the user has just been federated with" - def fetch_initial_posts(user) do - pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) - - Enum.each( - # Insert all the posts in reverse order, so they're in the right order on the timeline - Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), - &Pleroma.Web.Federator.incoming_ap_doc/1 - ) - end + def fetch_initial_posts(user), + do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user]) @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() def get_followers_query(%User{} = user, nil) do @@ -735,6 +698,18 @@ defmodule Pleroma.User do end end + def remove_duplicated_following(%User{following: following} = user) do + uniq_following = Enum.uniq(following) + + if length(following) == length(uniq_following) do + {:ok, user} + else + user + |> update_changeset(%{following: uniq_following}) + |> update_and_set_cache() + end + end + @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do criteria = %{ap_id: ap_ids, deactivated: false} @@ -750,139 +725,6 @@ 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: 20 - ) - 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) - - 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 blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do - Enum.map( - blocked_identifiers, - fn blocked_identifier -> - with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, blocker} <- block(blocker, blocked), - {:ok, _} <- ActivityPub.block(blocker, blocked) do - blocked - else - err -> - Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}") - err - end - end - ) - end - def mute(muter, %User{ap_id: ap_id}) do info_cng = muter.info @@ -994,15 +836,12 @@ 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 + def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do + blocks = info.blocks + domain_blocks = info.domain_blocks %{host: host} = URI.parse(ap_id) - Enum.member?(blocks, ap_id) || - Enum.any?(domain_blocks, fn domain -> - host == domain - end) + Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host)) end def subscribed_to?(user, %{ap_id: ap_id}) do @@ -1057,8 +896,6 @@ defmodule Pleroma.User do PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status]) end - def perform(:deactivate_async, user, status), do: deactivate(user, status) - def deactivate(%User{} = user, status \\ true) do info_cng = User.Info.set_activation_status(user.info, status) @@ -1090,34 +927,125 @@ defmodule Pleroma.User do @spec perform(atom(), User.t()) :: {:ok, User.t()} def perform(:delete, %User{} = user) do - {:ok, user} = User.deactivate(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) + + {:ok, _user} = Repo.delete(user) end - def delete_user_activities(%User{ap_id: ap_id} = user) do - stream = - ap_id - |> Activity.query_by_actor() - |> Activity.with_preloaded_object() - |> Repo.stream() + @spec perform(atom(), User.t()) :: {:ok, User.t()} + def perform(:fetch_initial_posts, %User{} = user) do + pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) - Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) + Enum.each( + # Insert all the posts in reverse order, so they're in the right order on the timeline + Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), + &Pleroma.Web.Federator.incoming_ap_doc/1 + ) + + {:ok, user} + end + + def perform(:deactivate_async, user, status), do: deactivate(user, status) + + @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} + def perform(:blocks_import, %User{} = blocker, blocked_identifiers) + when is_list(blocked_identifiers) do + Enum.map( + blocked_identifiers, + fn blocked_identifier -> + with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), + {:ok, blocker} <- block(blocker, blocked), + {:ok, _} <- ActivityPub.block(blocker, blocked) do + blocked + else + err -> + Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}") + err + end + end + ) + end + + @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} + def perform(:follow_import, %User{} = follower, followed_identifiers) + when is_list(followed_identifiers) do + Enum.map( + followed_identifiers, + fn followed_identifier -> + with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), + {:ok, follower} <- maybe_direct_follow(follower, followed), + {:ok, _} <- ActivityPub.follow(follower, followed) do + followed + else + err -> + Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}") + err + end + end + ) + end + + def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers), + do: + PleromaJobQueue.enqueue(:background, __MODULE__, [ + :blocks_import, + blocker, + blocked_identifiers + ]) + + def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers), + do: + PleromaJobQueue.enqueue(:background, __MODULE__, [ + :follow_import, + follower, + followed_identifiers + ]) + + def delete_user_activities(%User{ap_id: ap_id} = user) do + 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" @@ -1126,9 +1054,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) @@ -1157,8 +1083,8 @@ defmodule Pleroma.User do resp = fetch_by_ap_id(ap_id) if should_fetch_initial do - with {:ok, %User{} = user} = resp do - {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) + with {:ok, %User{} = user} <- resp do + fetch_initial_posts(user) end end @@ -1354,4 +1280,75 @@ defmodule Pleroma.User do def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end + + @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()} + def toggle_confirmation(%User{} = user) do + need_confirmation? = !user.info.confirmation_pending + + info_changeset = + User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?) + + user + |> change() + |> put_embed(:info, info_changeset) + |> update_and_set_cache() + end + + def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do + mascot + end + + def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do + # use instance-default + config = Pleroma.Config.get([:assets, :mascots]) + default_mascot = Pleroma.Config.get([:assets, :default_mascot]) + mascot = Keyword.get(config, default_mascot) + + %{ + "id" => "default-mascot", + "url" => mascot[:url], + "preview_url" => mascot[:url], + "pleroma" => %{ + "mime_type" => mascot[:mime_type] + } + } + end + + def ensure_keys_present(user) do + info = user.info + + if info.keys do + {:ok, user} + else + {:ok, pem} = Keys.generate_rsa_pem() + + info_cng = + info + |> User.Info.set_keys(pem) + + cng = + Ecto.Changeset.change(user) + |> Ecto.Changeset.put_embed(:info, info_cng) + + update_and_set_cache(cng) + 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 + + defdelegate search(query, opts \\ []), to: User.Search + + 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 end