X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fuser.ex;h=5705098eaea236392f6546b4e35d147b54b7daf7;hb=dd8f2196f62ab4d4cdec67bdb2b434a317a3f396;hp=bed8fd1b46d57bbc5851953b18d0468a968bb8e4;hpb=d0ec2812bd347469ebce8a4606953bd6c901db80;p=akkoma diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index bed8fd1b4..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.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) @@ -21,6 +36,7 @@ defmodule Pleroma.User do field(:local, :boolean, default: true) 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) @@ -28,6 +44,21 @@ defmodule Pleroma.User do 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 @@ -60,10 +91,6 @@ defmodule Pleroma.User do |> validate_required([:following]) end - def info_changeset(struct, params \\ %{}) do - raise "NOT VALID ANYMORE" - end - def user_info(%User{} = user) do oneself = if user.local, do: 1, else: 0 @@ -72,11 +99,11 @@ defmodule Pleroma.User do 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 @@ -112,12 +139,11 @@ defmodule Pleroma.User do end end - # TODO: Check if this still used 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 @@ -134,7 +160,7 @@ defmodule Pleroma.User do struct |> 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) @@ -164,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]) @@ -172,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]) @@ -192,6 +229,25 @@ defmodule Pleroma.User do end end + @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 + def needs_update?(%User{local: true}), do: false def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true @@ -211,14 +267,14 @@ defmodule Pleroma.User do end def maybe_direct_follow(%User{} = follower, %User{} = followed) do - if !User.ap_enabled?(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 @@ -280,10 +336,29 @@ 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 end @@ -320,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 @@ -549,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 = @@ -602,6 +698,9 @@ 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 info_cng = user.info @@ -683,7 +782,7 @@ defmodule Pleroma.User do {:ok, user} end - def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do + def html_filter_policy(%User{info: %{no_rich_text: true}}) do Pleroma.HTML.Scrubber.TwitterText end @@ -734,7 +833,8 @@ defmodule Pleroma.User do 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() @@ -772,13 +872,10 @@ defmodule Pleroma.User do 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) - else - get_or_fetch_by_nickname(uri_or_nickname) - end - end + @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 @@ -803,4 +900,71 @@ defmodule Pleroma.User do :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 + @strict_local_nickname_regex + end + end end