X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fuser.ex;h=5603d1f5f24f232dd0614c315d416fe1f883b577;hb=dd8f2196f62ab4d4cdec67bdb2b434a317a3f396;hp=1dad30e876f6265d5835e7604ded15aea50ca3a8;hpb=b0ec4f33e661cb14730a622d64dbc721e2723825;p=akkoma diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1dad30e87..5705098ea 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1,12 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.User do use Ecto.Schema import Ecto.{Changeset, Query} alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Comeonin.Pbkdf2 - alias Pleroma.Web.{OStatus, Websub} + alias Pleroma.Formatter + alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils + alias Pleroma.Web.{OStatus, Websub, OAuth} alias Pleroma.Web.ActivityPub.{Utils, ActivityPub} + require Logger + + @type t :: %__MODULE__{} + + @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + + @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ + @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/ + schema "users" do field(:bio, :string) field(:email, :string) @@ -19,14 +34,31 @@ defmodule Pleroma.User do field(:ap_id, :string) field(:avatar, :map) field(:local, :boolean, default: true) - field(:info, :map, default: %{}) field(:follower_address, :string) field(:search_distance, :float, virtual: true) + field(:tags, {:array, :string}, default: []) + field(:last_refreshed_at, :naive_datetime) has_many(:notifications, Notification) + embeds_one(:info, Pleroma.User.Info) timestamps() end + def auth_active?(%User{} = user) do + (user.info && !user.info.confirmation_pending) || + !Pleroma.Config.get([:instance, :account_activation_required]) + end + + def remote_or_auth_active?(%User{} = user), do: !user.local || auth_active?(user) + + def visible_for?(%User{} = user, for_user \\ nil) do + User.remote_or_auth_active?(user) || (for_user && for_user.id == user.id) || + User.superuser?(for_user) + end + + def superuser?(nil), do: false + def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info) + def avatar_url(user) do case user.avatar do %{"url" => [%{"href" => href} | _]} -> href @@ -35,12 +67,16 @@ defmodule Pleroma.User do end def banner_url(user) do - case user.info["banner"] do + case user.info.banner do %{"url" => [%{"href" => href} | _]} -> href _ -> "#{Web.base_url()}/images/banner.png" end end + def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url + def profile_url(%User{ap_id: ap_id}), do: ap_id + def profile_url(_), do: nil + def ap_id(%User{nickname: nickname}) do "#{Web.base_url()}/users/#{nickname}" end @@ -55,38 +91,39 @@ defmodule Pleroma.User do |> validate_required([:following]) end - def info_changeset(struct, params \\ %{}) do - struct - |> cast(params, [:info]) - |> validate_required([:info]) - end - def user_info(%User{} = user) do oneself = if user.local, do: 1, else: 0 %{ following_count: length(user.following) - oneself, - note_count: user.info["note_count"] || 0, - follower_count: user.info["follower_count"] || 0, - locked: user.info["locked"] || false, - default_scope: user.info["default_scope"] || "public" + 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 } end - @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ def remote_user_creation(params) do + params = + params + |> Map.put(:info, params[:info] || %{}) + + info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info]) + changes = %User{} - |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar]) + |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar]) |> validate_required([:name, :ap_id]) |> unique_constraint(:nickname) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: 5000) |> validate_length(:name, max: 100) |> put_change(:local, false) + |> put_embed(:info, info_cng) if changes.valid? do - case changes.changes[:info]["source_data"] do + case info_cng.changes[:source_data] do %{"followers" => followers} -> changes |> put_change(:follower_address, followers) @@ -104,20 +141,29 @@ defmodule Pleroma.User do def update_changeset(struct, params \\ %{}) do struct - |> cast(params, [:bio, :name]) + |> cast(params, [:bio, :name, :avatar]) |> unique_constraint(:nickname) - |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) + |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) |> validate_length(:name, min: 1, max: 100) end def upgrade_changeset(struct, params \\ %{}) do + params = + params + |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now()) + + info_cng = + struct.info + |> User.Info.user_upgrade(params[:info]) + struct - |> cast(params, [:bio, :name, :info, :follower_address, :avatar]) + |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at]) |> unique_constraint(:nickname) - |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) + |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) |> validate_length(:name, max: 100) + |> put_embed(:info, info_cng) end def password_update_changeset(struct, params) do @@ -127,6 +173,9 @@ defmodule Pleroma.User do |> 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]) @@ -141,7 +190,16 @@ defmodule Pleroma.User do update_and_set_cache(password_update_changeset(user, data)) end - def register_changeset(struct, params \\ %{}) do + def register_changeset(struct, params \\ %{}, opts \\ []) do + confirmation_status = + if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do + :confirmed + else + :unconfirmed + end + + info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) + changeset = struct |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) @@ -149,10 +207,12 @@ defmodule Pleroma.User do |> validate_confirmation(:password) |> unique_constraint(:email) |> unique_constraint(:nickname) - |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) + |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) + |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) |> validate_length(:bio, max: 1000) |> validate_length(:name, min: 1, max: 100) + |> put_change(:info, info_change) if changeset.valid? do hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) @@ -169,40 +229,52 @@ defmodule Pleroma.User do end end - def maybe_direct_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) + @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" + def register(%Ecto.Changeset{} = changeset) do + with {:ok, user} <- Repo.insert(changeset), + {:ok, _} = try_send_confirmation_email(user) do + {:ok, user} + end + end + + def try_send_confirmation_email(%User{} = user) do + if user.info.confirmation_pending && + Pleroma.Config.get([:instance, :account_activation_required]) do + user + |> Pleroma.UserEmail.account_confirmation_email() + |> Pleroma.Mailer.deliver() + else + {:ok, :noop} + end + end - user_info = user_info(followed) + def needs_update?(%User{local: true}), do: false - should_direct_follow = - cond do - # if the account is locked, don't pre-create the relationship - user_info[:locked] == true -> - false + def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true - # if the users are blocking each other, we shouldn't even be here, but check for it anyway - deny_follow_blocked and - (User.blocks?(follower, followed) or User.blocks?(followed, follower)) -> - false + def needs_update?(%User{local: false} = user) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400 + end - # if OStatus, then there is no three-way handshake to follow - User.ap_enabled?(followed) != true -> - true + def needs_update?(_), do: true - # if there are no other reasons not to, just pre-create the relationship - true -> - true - end + def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do + {:ok, follower} + end - if should_direct_follow do + def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do + follow(follower, followed) + end + + def maybe_direct_follow(%User{} = follower, %User{} = followed) do + if not User.ap_enabled?(followed) do follow(follower, followed) else {:ok, follower} end end - def maybe_follow(%User{} = follower, %User{info: info} = followed) do + def maybe_follow(%User{} = follower, %User{info: _info} = followed) do if not following?(follower, followed) do follow(follower, followed) else @@ -217,7 +289,7 @@ defmodule Pleroma.User do ap_followers = followed.follower_address cond do - following?(follower, followed) or info["deactivated"] -> + following?(follower, followed) or info.deactivated -> {:error, "Could not follow user: #{followed.nickname} is already on your list."} deny_follow_blocked and blocks?(followed, follower) -> @@ -264,12 +336,31 @@ defmodule Pleroma.User do end end + @spec following?(User.t(), User.t()) :: boolean def following?(%User{} = follower, %User{} = followed) do Enum.member?(follower.following, followed.follower_address) end + def follow_import(%User{} = follower, followed_identifiers) + when is_list(followed_identifiers) do + Enum.map( + followed_identifiers, + fn followed_identifier -> + with %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 + user.info.locked || false end def get_by_ap_id(ap_id) do @@ -290,6 +381,7 @@ defmodule Pleroma.User do def invalidate_cache(user) do Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "nickname:#{user.nickname}") + Cachex.del(:user_cache, "user_info:#{user.id}") end def get_cached_by_ap_id(ap_id) do @@ -303,7 +395,11 @@ defmodule Pleroma.User do end def get_by_nickname(nickname) do - Repo.get_by(User, nickname: nickname) + Repo.get_by(User, nickname: nickname) || + if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do + [local_nickname, _] = String.split(nickname, "@") + Repo.get_by(User, nickname: local_nickname) + end end def get_by_nickname_or_email(nickname_or_email) do @@ -405,22 +501,23 @@ defmodule Pleroma.User do end def increase_note_count(%User{} = user) do - note_count = (user.info["note_count"] || 0) + 1 - new_info = Map.put(user.info, "note_count", note_count) + info_cng = User.Info.add_to_note_count(user.info, 1) - cs = info_changeset(user, %{info: new_info}) + cng = + change(user) + |> put_embed(:info, info_cng) - update_and_set_cache(cs) + update_and_set_cache(cng) end def decrease_note_count(%User{} = user) do - note_count = user.info["note_count"] || 0 - note_count = if note_count <= 0, do: 0, else: note_count - 1 - new_info = Map.put(user.info, "note_count", note_count) + info_cng = User.Info.add_to_note_count(user.info, -1) - cs = info_changeset(user, %{info: new_info}) + cng = + change(user) + |> put_embed(:info, info_cng) - update_and_set_cache(cs) + update_and_set_cache(cng) end def update_note_count(%User{} = user) do @@ -433,11 +530,13 @@ defmodule Pleroma.User do note_count = Repo.one(note_count_query) - new_info = Map.put(user.info, "note_count", note_count) + info_cng = User.Info.set_note_count(user.info, note_count) - cs = info_changeset(user, %{info: new_info}) + cng = + change(user) + |> put_embed(:info, info_cng) - update_and_set_cache(cs) + update_and_set_cache(cng) end def update_follower_count(%User{} = user) do @@ -451,43 +550,36 @@ defmodule Pleroma.User do follower_count = Repo.one(follower_count_query) - new_info = Map.put(user.info, "follower_count", follower_count) + info_cng = + user.info + |> User.Info.set_follower_count(follower_count) - cs = info_changeset(user, %{info: new_info}) + cng = + change(user) + |> put_embed(:info, info_cng) - update_and_set_cache(cs) + update_and_set_cache(cng) end - def get_notified_from_activity_query(to) do + def get_users_from_set_query(ap_ids, false) do from( u in User, - where: u.ap_id in ^to, - where: u.local == true + where: u.ap_id in ^ap_ids ) end - def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do - object = Object.normalize(data["object"]) - actor = User.get_cached_by_ap_id(data["actor"]) + def get_users_from_set_query(ap_ids, true) do + query = get_users_from_set_query(ap_ids, false) - # ensure that the actor who published the announced object appears only once - to = - if actor.nickname != nil do - to ++ [object.data["actor"]] - else - to - end - |> Enum.uniq() - - query = get_notified_from_activity_query(to) - - Repo.all(query) + from( + u in query, + where: u.local == true + ) end - def get_notified_from_activity(%Activity{recipients: to}) do - query = get_notified_from_activity_query(to) - - Repo.all(query) + def get_users_from_set(ap_ids, local_only \\ true) do + get_users_from_set_query(ap_ids, local_only) + |> Repo.all() end def get_recipients_from_activity(%Activity{recipients: to}) do @@ -503,7 +595,7 @@ defmodule Pleroma.User do Repo.all(query) end - def search(query, resolve) do + def search(query, resolve \\ false) do # strip the beginning @ off if there is a query query = String.trim_leading(query, "@") @@ -536,6 +628,23 @@ defmodule Pleroma.User do Repo.all(q) end + def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do + Enum.map( + blocked_identifiers, + fn blocked_identifier -> + with %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 block(blocker, %User{ap_id: ap_id} = blocked) do # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) blocker = @@ -550,12 +659,15 @@ defmodule Pleroma.User do unfollow(blocked, blocker) end - blocks = blocker.info["blocks"] || [] - new_blocks = Enum.uniq([ap_id | blocks]) - new_info = Map.put(blocker.info, "blocks", new_blocks) + info_cng = + blocker.info + |> User.Info.add_to_block(ap_id) - cs = User.info_changeset(blocker, %{info: new_info}) - update_and_set_cache(cs) + cng = + change(blocker) + |> put_embed(:info, info_cng) + + update_and_set_cache(cng) end # helper to handle the block given only an actor's AP id @@ -563,18 +675,21 @@ defmodule Pleroma.User do block(blocker, User.get_by_ap_id(ap_id)) end - def unblock(user, %{ap_id: ap_id}) do - blocks = user.info["blocks"] || [] - new_blocks = List.delete(blocks, ap_id) - new_info = Map.put(user.info, "blocks", new_blocks) + def unblock(blocker, %{ap_id: ap_id}) do + info_cng = + blocker.info + |> User.Info.remove_from_block(ap_id) + + cng = + change(blocker) + |> put_embed(:info, info_cng) - cs = User.info_changeset(user, %{info: new_info}) - update_and_set_cache(cs) + update_and_set_cache(cng) end def blocks?(user, %{ap_id: ap_id}) do - blocks = user.info["blocks"] || [] - domain_blocks = user.info["domain_blocks"] || [] + blocks = user.info.blocks + domain_blocks = user.info.domain_blocks %{host: host} = URI.parse(ap_id) Enum.member?(blocks, ap_id) || @@ -583,22 +698,31 @@ defmodule Pleroma.User do end) end + def blocked_users(user), + do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) + def block_domain(user, domain) do - domain_blocks = user.info["domain_blocks"] || [] - new_blocks = Enum.uniq([domain | domain_blocks]) - new_info = Map.put(user.info, "domain_blocks", new_blocks) + info_cng = + user.info + |> User.Info.add_to_domain_block(domain) - cs = User.info_changeset(user, %{info: new_info}) - update_and_set_cache(cs) + cng = + change(user) + |> put_embed(:info, info_cng) + + update_and_set_cache(cng) end def unblock_domain(user, domain) do - blocks = user.info["domain_blocks"] || [] - new_blocks = List.delete(blocks, domain) - new_info = Map.put(user.info, "domain_blocks", new_blocks) + info_cng = + user.info + |> User.Info.remove_from_domain_block(domain) + + cng = + change(user) + |> put_embed(:info, info_cng) - cs = User.info_changeset(user, %{info: new_info}) - update_and_set_cache(cs) + update_and_set_cache(cng) end def local_user_query() do @@ -617,10 +741,14 @@ defmodule Pleroma.User do ) end - def deactivate(%User{} = user) do - new_info = Map.put(user.info, "deactivated", true) - cs = User.info_changeset(user, %{info: new_info}) - update_and_set_cache(cs) + def deactivate(%User{} = user, status \\ true) do + info_cng = User.Info.set_activation_status(user.info, status) + + cng = + change(user) + |> put_embed(:info, info_cng) + + update_and_set_cache(cng) end def delete(%User{} = user) do @@ -651,11 +779,19 @@ defmodule Pleroma.User do end end) - :ok + {:ok, user} end + def html_filter_policy(%User{info: %{no_rich_text: true}}) do + Pleroma.HTML.Scrubber.TwitterText + end + + def html_filter_policy(_), do: nil + def get_or_fetch_by_ap_id(ap_id) do - if user = get_by_ap_id(ap_id) do + user = get_by_ap_id(ap_id) + + if !is_nil(user) and !User.needs_update?(user) do user else ap_try = ActivityPub.make_user_from_ap_id(ap_id) @@ -680,7 +816,7 @@ defmodule Pleroma.User do user else changes = - %User{} + %User{info: %User.Info{}} |> cast(%{}, [:ap_id, :nickname, :local]) |> put_change(:ap_id, relay_uri) |> put_change(:nickname, nil) @@ -694,10 +830,11 @@ defmodule Pleroma.User do # AP style def public_key_from_info(%{ - "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}} + source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}} }) do key = - :public_key.pem_decode(public_key_pem) + public_key_pem + |> :public_key.pem_decode() |> hd() |> :public_key.pem_entry_decode() @@ -705,7 +842,7 @@ 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}) do {:ok, Pleroma.Web.Salmon.decode_key(magic_key)} end @@ -727,17 +864,107 @@ defmodule Pleroma.User do |> Map.put(:name, blank?(data[:name]) || data[:nickname]) cs = User.remote_user_creation(data) + Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname) end - def ap_enabled?(%User{info: info}), do: info["ap_enabled"] + def ap_enabled?(%User{local: true}), do: true + def ap_enabled?(%User{info: info}), do: info.ap_enabled def ap_enabled?(_), do: false - def get_or_fetch(uri_or_nickname) do - if String.starts_with?(uri_or_nickname, "http") do - get_or_fetch_by_ap_id(uri_or_nickname) + @doc "Gets or fetch a user by uri or nickname." + @spec get_or_fetch(String.t()) :: User.t() + def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) + def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname) + + # wait a period of time and return newest version of the User structs + # this is because we have synchronous follow APIs and need to simulate them + # with an async handshake + def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do + with %User{} = a <- Repo.get(User, a.id), + %User{} = b <- Repo.get(User, b.id) do + {:ok, a, b} + else + _e -> + :error + end + end + + def wait_and_refresh(timeout, %User{} = a, %User{} = b) do + with :ok <- :timer.sleep(timeout), + %User{} = a <- Repo.get(User, a.id), + %User{} = b <- Repo.get(User, b.id) do + {:ok, a, b} + else + _e -> + :error + end + end + + def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) + def parse_bio(nil, _user), do: "" + def parse_bio(bio, _user) when bio == "", do: bio + + def parse_bio(bio, user) do + mentions = Formatter.parse_mentions(bio) + tags = Formatter.parse_tags(bio) + + emoji = + (user.info.source_data["tag"] || []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} + end) + + bio + |> CommonUtils.format_input(mentions, tags, "text/plain") + |> Formatter.emojify(emoji) + end + + def tag(user_identifiers, tags) when is_list(user_identifiers) do + Repo.transaction(fn -> + for user_identifier <- user_identifiers, do: tag(user_identifier, tags) + end) + end + + def tag(nickname, tags) when is_binary(nickname), + do: tag(User.get_by_nickname(nickname), tags) + + def tag(%User{} = user, tags), + do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags))) + + def untag(user_identifiers, tags) when is_list(user_identifiers) do + Repo.transaction(fn -> + for user_identifier <- user_identifiers, do: untag(user_identifier, tags) + end) + end + + def untag(nickname, tags) when is_binary(nickname), + do: untag(User.get_by_nickname(nickname), tags) + + def untag(%User{} = user, tags), + do: update_tags(user, (user.tags || []) -- normalize_tags(tags)) + + defp update_tags(%User{} = user, new_tags) do + {:ok, updated_user} = + user + |> change(%{tags: new_tags}) + |> Repo.update() + + updated_user + end + + defp normalize_tags(tags) do + [tags] + |> List.flatten() + |> Enum.map(&String.downcase(&1)) + end + + defp local_nickname_regex() do + if Pleroma.Config.get([:instance, :extended_nickname_format]) do + @extended_local_nickname_regex else - get_or_fetch_by_nickname(uri_or_nickname) + @strict_local_nickname_regex end end end