alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.OStatus
alias Pleroma.Web.RelMe
alias Pleroma.Web.Websub
+ alias Pleroma.Workers.BackgroundWorker
require Logger
def user_info(%User{} = user, args \\ %{}) do
following_count =
- if args[:following_count], do: args[:following_count], else: following_count(user)
+ 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
|> Map.put(:follower_count, follower_count)
end
+ def follow_state(%User{} = user, %User{} = target) do
+ follow_activity = Utils.fetch_latest_follow(user, target)
+
+ if follow_activity,
+ do: follow_activity.data["state"],
+ # Ideally this would be nil, but then Cachex does not commit the value
+ else: false
+ end
+
+ def get_cached_follow_state(user, target) do
+ key = "follow_state:#{user.ap_id}|#{target.ap_id}"
+ Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
+ end
+
+ def set_follow_state_cache(user_ap_id, target_ap_id, state) do
+ Cachex.put(
+ :user_cache,
+ "follow_state:#{user_ap_id}|#{target_ap_id}",
+ state
+ )
+ end
+
def set_info_cache(user, args) do
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
end
end
def remote_user_creation(params) do
- params =
- params
- |> Map.put(:info, params[:info] || %{})
+ bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ params = Map.put(params, :info, params[:info] || %{})
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
changes =
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
- |> validate_length(:bio, max: 5000)
- |> validate_length(:name, max: 100)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, max: name_limit)
|> put_change(:local, false)
|> put_embed(:info, info_cng)
end
def update_changeset(struct, params \\ %{}) do
+ bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+
struct
|> cast(params, [:bio, :name, :avatar, :following])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
- |> validate_length(:bio, max: 5000)
- |> validate_length(:name, min: 1, max: 100)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, min: 1, max: name_limit)
end
- def upgrade_changeset(struct, params \\ %{}) do
- params =
- params
- |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
+ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
+ bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
- info_cng =
- struct.info
- |> User.Info.user_upgrade(params[:info])
+ params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
+ info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
struct
|> cast(params, [
])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
- |> validate_length(:bio, max: 5000)
- |> validate_length(:name, max: 100)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, max: name_limit)
|> put_embed(:info, info_cng)
end
|> 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()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
+ bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
Pleroma.Config.get([:instance, :account_activation_required])
|> 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)
- |> validate_length(:name, min: 1, max: 100)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, min: 1, max: name_limit)
|> put_change(:info, info_change)
changeset =
@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, user} <- autofollow_users(user),
+ {:ok, user} <- post_register_action(user) do
+ {:ok, user}
+ end
+ end
+
+ def post_register_action(%User{} = user) do
+ with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(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
{1, [follower]} = Repo.update_all(q, [])
+ follower = maybe_update_following_count(follower)
+
{:ok, _} = update_follower_count(followed)
set_cache(follower)
{1, [follower]} = Repo.update_all(q, [])
+ follower = maybe_update_following_count(follower)
+
{:ok, followed} = update_follower_count(followed)
set_cache(follower)
Repo.get_by(User, ap_id: ap_id)
end
+ def get_all_by_ap_id(ap_ids) do
+ from(u in __MODULE__,
+ where: u.ap_id in ^ap_ids
+ )
+ |> Repo.all()
+ end
+
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) 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
end)
end
- def get_cached_by_nickname_or_id(nickname_or_id) do
- get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
+ restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+
+ cond do
+ is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
+ get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+
+ restrict_to_local == false ->
+ get_cached_by_nickname(nickname_or_id)
+
+ restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
+ get_cached_by_nickname(nickname_or_id)
+
+ true ->
+ nil
+ end
end
def get_by_nickname(nickname) do
end
@doc "Fetch some posts when the user has just been federated with"
- def fetch_initial_posts(user),
- do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
+ def fetch_initial_posts(user) do
+ BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
+ end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) 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)
|> update_and_set_cache()
end
+ @spec maybe_fetch_follow_information(User.t()) :: User.t()
+ 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}
+ @spec maybe_update_following_count(User.t()) :: User.t()
+ def maybe_update_following_count(%User{local: false} = user) do
+ if Pleroma.Config.get([:instance, :external_user_synchronization]) do
+ 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)
blocker
end
+ # clear any requested follows as well
+ blocked =
+ case CommonAPI.reject_follow_request(blocked, blocker) do
+ {:ok, %User{} = updated_blocked} -> updated_blocked
+ nil -> blocked
+ end
+
blocker =
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
def muted_notifications?(user, %{ap_id: ap_id}),
do: Enum.member?(user.info.muted_notifications, ap_id)
- 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)
+ def blocks?(%User{} = user, %User{} = target) do
+ blocks_ap_id?(user, target) || blocks_domain?(user, target)
+ end
+
+ def blocks?(nil, _), do: false
- Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
+ 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)
end
def deactivate_async(user, status \\ true) do
- PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
+ BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
def deactivate(%User{} = user, status \\ true) do
|> update_and_set_cache()
end
- @spec delete(User.t()) :: :ok
- def delete(%User{} = user),
- do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
+ def delete(%User{} = user) do
+ BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
+ end
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
Repo.all(query)
end
- def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
- do:
- PleromaJobQueue.enqueue(:background, __MODULE__, [
- :blocks_import,
- blocker,
- blocked_identifiers
- ])
+ def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
+ BackgroundWorker.enqueue("blocks_import", %{
+ "blocker_id" => blocker.id,
+ "blocked_identifiers" => blocked_identifiers
+ })
+ end
- def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
- do:
- PleromaJobQueue.enqueue(:background, __MODULE__, [
- :follow_import,
- follower,
- followed_identifiers
- ])
+ def follow_import(%User{} = follower, followed_identifiers)
+ when is_list(followed_identifiers) do
+ BackgroundWorker.enqueue("follow_import", %{
+ "follower_id" => follower.id,
+ "followed_identifiers" => followed_identifiers
+ })
+ end
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
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