healthcheck: report real amount of memory allocated by beam
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10 import Ecto, only: [assoc: 2]
11
12 alias Comeonin.Pbkdf2
13 alias Ecto.Multi
14 alias Pleroma.Activity
15 alias Pleroma.Config
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
19 alias Pleroma.Formatter
20 alias Pleroma.HTML
21 alias Pleroma.Keys
22 alias Pleroma.Notification
23 alias Pleroma.Object
24 alias Pleroma.Registration
25 alias Pleroma.Repo
26 alias Pleroma.RepoStreamer
27 alias Pleroma.User
28 alias Pleroma.UserRelationship
29 alias Pleroma.Web
30 alias Pleroma.Web.ActivityPub.ActivityPub
31 alias Pleroma.Web.ActivityPub.Utils
32 alias Pleroma.Web.CommonAPI
33 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
34 alias Pleroma.Web.OAuth
35 alias Pleroma.Web.RelMe
36 alias Pleroma.Workers.BackgroundWorker
37
38 require Logger
39
40 @type t :: %__MODULE__{}
41 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
42 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
43
44 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
45 @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])?)*$/
46
47 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
48 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
49
50 # AP ID user relationships (blocks, mutes etc.)
51 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
52 @user_relationships_config [
53 block: [
54 blocker_blocks: :blocked_users,
55 blockee_blocks: :blocker_users
56 ],
57 mute: [
58 muter_mutes: :muted_users,
59 mutee_mutes: :muter_users
60 ],
61 reblog_mute: [
62 reblog_muter_mutes: :reblog_muted_users,
63 reblog_mutee_mutes: :reblog_muter_users
64 ],
65 notification_mute: [
66 notification_muter_mutes: :notification_muted_users,
67 notification_mutee_mutes: :notification_muter_users
68 ],
69 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
70 inverse_subscription: [
71 subscribee_subscriptions: :subscriber_users,
72 subscriber_subscriptions: :subscribee_users
73 ]
74 ]
75
76 schema "users" do
77 field(:bio, :string)
78 field(:email, :string)
79 field(:name, :string)
80 field(:nickname, :string)
81 field(:password_hash, :string)
82 field(:password, :string, virtual: true)
83 field(:password_confirmation, :string, virtual: true)
84 field(:keys, :string)
85 field(:ap_id, :string)
86 field(:avatar, :map)
87 field(:local, :boolean, default: true)
88 field(:follower_address, :string)
89 field(:following_address, :string)
90 field(:search_rank, :float, virtual: true)
91 field(:search_type, :integer, virtual: true)
92 field(:tags, {:array, :string}, default: [])
93 field(:last_refreshed_at, :naive_datetime_usec)
94 field(:last_digest_emailed_at, :naive_datetime)
95 field(:banner, :map, default: %{})
96 field(:background, :map, default: %{})
97 field(:source_data, :map, default: %{})
98 field(:note_count, :integer, default: 0)
99 field(:follower_count, :integer, default: 0)
100 field(:following_count, :integer, default: 0)
101 field(:locked, :boolean, default: false)
102 field(:confirmation_pending, :boolean, default: false)
103 field(:password_reset_pending, :boolean, default: false)
104 field(:confirmation_token, :string, default: nil)
105 field(:default_scope, :string, default: "public")
106 field(:domain_blocks, {:array, :string}, default: [])
107 field(:deactivated, :boolean, default: false)
108 field(:no_rich_text, :boolean, default: false)
109 field(:ap_enabled, :boolean, default: false)
110 field(:is_moderator, :boolean, default: false)
111 field(:is_admin, :boolean, default: false)
112 field(:show_role, :boolean, default: true)
113 field(:settings, :map, default: nil)
114 field(:magic_key, :string, default: nil)
115 field(:uri, :string, default: nil)
116 field(:hide_followers_count, :boolean, default: false)
117 field(:hide_follows_count, :boolean, default: false)
118 field(:hide_followers, :boolean, default: false)
119 field(:hide_follows, :boolean, default: false)
120 field(:hide_favorites, :boolean, default: true)
121 field(:unread_conversation_count, :integer, default: 0)
122 field(:pinned_activities, {:array, :string}, default: [])
123 field(:email_notifications, :map, default: %{"digest" => false})
124 field(:mascot, :map, default: nil)
125 field(:emoji, {:array, :map}, default: [])
126 field(:pleroma_settings_store, :map, default: %{})
127 field(:fields, {:array, :map}, default: [])
128 field(:raw_fields, {:array, :map}, default: [])
129 field(:discoverable, :boolean, default: false)
130 field(:invisible, :boolean, default: false)
131 field(:allow_following_move, :boolean, default: true)
132 field(:skip_thread_containment, :boolean, default: false)
133 field(:actor_type, :string, default: "Person")
134 field(:also_known_as, {:array, :string}, default: [])
135
136 embeds_one(
137 :notification_settings,
138 Pleroma.User.NotificationSetting,
139 on_replace: :update
140 )
141
142 has_many(:notifications, Notification)
143 has_many(:registrations, Registration)
144 has_many(:deliveries, Delivery)
145
146 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
147 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
148
149 for {relationship_type,
150 [
151 {outgoing_relation, outgoing_relation_target},
152 {incoming_relation, incoming_relation_source}
153 ]} <- @user_relationships_config do
154 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
155 # :notification_muter_mutes, :subscribee_subscriptions
156 has_many(outgoing_relation, UserRelationship,
157 foreign_key: :source_id,
158 where: [relationship_type: relationship_type]
159 )
160
161 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
162 # :notification_mutee_mutes, :subscriber_subscriptions
163 has_many(incoming_relation, UserRelationship,
164 foreign_key: :target_id,
165 where: [relationship_type: relationship_type]
166 )
167
168 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
169 # :notification_muted_users, :subscriber_users
170 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
171
172 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
173 # :notification_muter_users, :subscribee_users
174 has_many(incoming_relation_source, through: [incoming_relation, :source])
175 end
176
177 # `:blocks` is deprecated (replaced with `blocked_users` relation)
178 field(:blocks, {:array, :string}, default: [])
179 # `:mutes` is deprecated (replaced with `muted_users` relation)
180 field(:mutes, {:array, :string}, default: [])
181 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
182 field(:muted_reblogs, {:array, :string}, default: [])
183 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
184 field(:muted_notifications, {:array, :string}, default: [])
185 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
186 field(:subscribers, {:array, :string}, default: [])
187
188 timestamps()
189 end
190
191 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
192 @user_relationships_config do
193 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
194 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
195 # `def subscriber_users/2`
196 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
197 target_users_query = assoc(user, unquote(outgoing_relation_target))
198
199 if restrict_deactivated? do
200 restrict_deactivated(target_users_query)
201 else
202 target_users_query
203 end
204 end
205
206 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
207 # `def notification_muted_users/2`, `def subscriber_users/2`
208 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
209 __MODULE__
210 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
211 user,
212 restrict_deactivated?
213 ])
214 |> Repo.all()
215 end
216
217 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
218 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
219 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
220 __MODULE__
221 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
222 user,
223 restrict_deactivated?
224 ])
225 |> select([u], u.ap_id)
226 |> Repo.all()
227 end
228 end
229
230 @doc """
231 Dumps Flake Id to SQL-compatible format (16-byte UUID).
232 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
233 """
234 def binary_id(source_id) when is_binary(source_id) do
235 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
236 dumped_id
237 else
238 _ -> source_id
239 end
240 end
241
242 def binary_id(source_ids) when is_list(source_ids) do
243 Enum.map(source_ids, &binary_id/1)
244 end
245
246 def binary_id(%User{} = user), do: binary_id(user.id)
247
248 @doc "Returns status account"
249 @spec account_status(User.t()) :: account_status()
250 def account_status(%User{deactivated: true}), do: :deactivated
251 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
252
253 def account_status(%User{confirmation_pending: true}) do
254 case Config.get([:instance, :account_activation_required]) do
255 true -> :confirmation_pending
256 _ -> :active
257 end
258 end
259
260 def account_status(%User{}), do: :active
261
262 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
263 def visible_for?(user, for_user \\ nil)
264
265 def visible_for?(%User{invisible: true}, _), do: false
266
267 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
268
269 def visible_for?(%User{} = user, for_user) do
270 account_status(user) == :active || superuser?(for_user)
271 end
272
273 def visible_for?(_, _), do: false
274
275 @spec superuser?(User.t()) :: boolean()
276 def superuser?(%User{local: true, is_admin: true}), do: true
277 def superuser?(%User{local: true, is_moderator: true}), do: true
278 def superuser?(_), do: false
279
280 @spec invisible?(User.t()) :: boolean()
281 def invisible?(%User{invisible: true}), do: true
282 def invisible?(_), do: false
283
284 def avatar_url(user, options \\ []) do
285 case user.avatar do
286 %{"url" => [%{"href" => href} | _]} -> href
287 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
288 end
289 end
290
291 def banner_url(user, options \\ []) do
292 case user.banner do
293 %{"url" => [%{"href" => href} | _]} -> href
294 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
295 end
296 end
297
298 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
299
300 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
301 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
302
303 @spec ap_following(User.t()) :: String.t()
304 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
305 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
306
307 def follow_state(%User{} = user, %User{} = target) do
308 case Utils.fetch_latest_follow(user, target) do
309 %{data: %{"state" => state}} -> state
310 # Ideally this would be nil, but then Cachex does not commit the value
311 _ -> false
312 end
313 end
314
315 def get_cached_follow_state(user, target) do
316 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
317 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
318 end
319
320 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
321 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
322 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
323 end
324
325 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
326 def restrict_deactivated(query) do
327 from(u in query, where: u.deactivated != ^true)
328 end
329
330 defdelegate following_count(user), to: FollowingRelationship
331
332 defp truncate_fields_param(params) do
333 if Map.has_key?(params, :fields) do
334 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
335 else
336 params
337 end
338 end
339
340 defp truncate_if_exists(params, key, max_length) do
341 if Map.has_key?(params, key) and is_binary(params[key]) do
342 {value, _chopped} = String.split_at(params[key], max_length)
343 Map.put(params, key, value)
344 else
345 params
346 end
347 end
348
349 def remote_user_creation(params) do
350 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
351 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
352
353 params =
354 params
355 |> truncate_if_exists(:name, name_limit)
356 |> truncate_if_exists(:bio, bio_limit)
357 |> truncate_fields_param()
358
359 changeset =
360 %User{local: false}
361 |> cast(
362 params,
363 [
364 :bio,
365 :name,
366 :ap_id,
367 :nickname,
368 :avatar,
369 :ap_enabled,
370 :source_data,
371 :banner,
372 :locked,
373 :magic_key,
374 :uri,
375 :hide_followers,
376 :hide_follows,
377 :hide_followers_count,
378 :hide_follows_count,
379 :follower_count,
380 :fields,
381 :following_count,
382 :discoverable,
383 :invisible,
384 :actor_type,
385 :also_known_as
386 ]
387 )
388 |> validate_required([:name, :ap_id])
389 |> unique_constraint(:nickname)
390 |> validate_format(:nickname, @email_regex)
391 |> validate_length(:bio, max: bio_limit)
392 |> validate_length(:name, max: name_limit)
393 |> validate_fields(true)
394
395 case params[:source_data] do
396 %{"followers" => followers, "following" => following} ->
397 changeset
398 |> put_change(:follower_address, followers)
399 |> put_change(:following_address, following)
400
401 _ ->
402 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
403 put_change(changeset, :follower_address, followers)
404 end
405 end
406
407 def update_changeset(struct, params \\ %{}) do
408 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
409 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
410
411 struct
412 |> cast(
413 params,
414 [
415 :bio,
416 :name,
417 :avatar,
418 :locked,
419 :no_rich_text,
420 :default_scope,
421 :banner,
422 :hide_follows,
423 :hide_followers,
424 :hide_followers_count,
425 :hide_follows_count,
426 :hide_favorites,
427 :allow_following_move,
428 :background,
429 :show_role,
430 :skip_thread_containment,
431 :fields,
432 :raw_fields,
433 :pleroma_settings_store,
434 :discoverable,
435 :actor_type,
436 :also_known_as
437 ]
438 )
439 |> unique_constraint(:nickname)
440 |> validate_format(:nickname, local_nickname_regex())
441 |> validate_length(:bio, max: bio_limit)
442 |> validate_length(:name, min: 1, max: name_limit)
443 |> put_fields()
444 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
445 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
446 |> put_change_if_present(:banner, &put_upload(&1, :banner))
447 |> put_change_if_present(:background, &put_upload(&1, :background))
448 |> put_change_if_present(
449 :pleroma_settings_store,
450 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
451 )
452 |> validate_fields(false)
453 end
454
455 defp put_fields(changeset) do
456 if raw_fields = get_change(changeset, :raw_fields) do
457 raw_fields =
458 raw_fields
459 |> Enum.filter(fn %{"name" => n} -> n != "" end)
460
461 fields =
462 raw_fields
463 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
464
465 changeset
466 |> put_change(:raw_fields, raw_fields)
467 |> put_change(:fields, fields)
468 else
469 changeset
470 end
471 end
472
473 defp parse_fields(value) do
474 value
475 |> Formatter.linkify(mentions_format: :full)
476 |> elem(0)
477 end
478
479 defp put_change_if_present(changeset, map_field, value_function) do
480 if value = get_change(changeset, map_field) do
481 with {:ok, new_value} <- value_function.(value) do
482 put_change(changeset, map_field, new_value)
483 else
484 _ -> changeset
485 end
486 else
487 changeset
488 end
489 end
490
491 defp put_upload(value, type) do
492 with %Plug.Upload{} <- value,
493 {:ok, object} <- ActivityPub.upload(value, type: type) do
494 {:ok, object.data}
495 end
496 end
497
498 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
499 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
500 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
501
502 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
503
504 params = if remote?, do: truncate_fields_param(params), else: params
505
506 struct
507 |> cast(
508 params,
509 [
510 :bio,
511 :name,
512 :follower_address,
513 :following_address,
514 :avatar,
515 :last_refreshed_at,
516 :ap_enabled,
517 :source_data,
518 :banner,
519 :locked,
520 :magic_key,
521 :follower_count,
522 :following_count,
523 :hide_follows,
524 :fields,
525 :hide_followers,
526 :allow_following_move,
527 :discoverable,
528 :hide_followers_count,
529 :hide_follows_count,
530 :actor_type,
531 :also_known_as
532 ]
533 )
534 |> unique_constraint(:nickname)
535 |> validate_format(:nickname, local_nickname_regex())
536 |> validate_length(:bio, max: bio_limit)
537 |> validate_length(:name, max: name_limit)
538 |> validate_fields(remote?)
539 end
540
541 def update_as_admin_changeset(struct, params) do
542 struct
543 |> update_changeset(params)
544 |> cast(params, [:email])
545 |> delete_change(:also_known_as)
546 |> unique_constraint(:email)
547 |> validate_format(:email, @email_regex)
548 end
549
550 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
551 def update_as_admin(user, params) do
552 params = Map.put(params, "password_confirmation", params["password"])
553 changeset = update_as_admin_changeset(user, params)
554
555 if params["password"] do
556 reset_password(user, changeset, params)
557 else
558 User.update_and_set_cache(changeset)
559 end
560 end
561
562 def password_update_changeset(struct, params) do
563 struct
564 |> cast(params, [:password, :password_confirmation])
565 |> validate_required([:password, :password_confirmation])
566 |> validate_confirmation(:password)
567 |> put_password_hash()
568 |> put_change(:password_reset_pending, false)
569 end
570
571 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
572 def reset_password(%User{} = user, params) do
573 reset_password(user, user, params)
574 end
575
576 def reset_password(%User{id: user_id} = user, struct, params) do
577 multi =
578 Multi.new()
579 |> Multi.update(:user, password_update_changeset(struct, params))
580 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
581 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
582
583 case Repo.transaction(multi) do
584 {:ok, %{user: user} = _} -> set_cache(user)
585 {:error, _, changeset, _} -> {:error, changeset}
586 end
587 end
588
589 def update_password_reset_pending(user, value) do
590 user
591 |> change()
592 |> put_change(:password_reset_pending, value)
593 |> update_and_set_cache()
594 end
595
596 def force_password_reset_async(user) do
597 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
598 end
599
600 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
601 def force_password_reset(user), do: update_password_reset_pending(user, true)
602
603 def register_changeset(struct, params \\ %{}, opts \\ []) do
604 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
605 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
606
607 need_confirmation? =
608 if is_nil(opts[:need_confirmation]) do
609 Pleroma.Config.get([:instance, :account_activation_required])
610 else
611 opts[:need_confirmation]
612 end
613
614 struct
615 |> confirmation_changeset(need_confirmation: need_confirmation?)
616 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
617 |> validate_required([:name, :nickname, :password, :password_confirmation])
618 |> validate_confirmation(:password)
619 |> unique_constraint(:email)
620 |> unique_constraint(:nickname)
621 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
622 |> validate_format(:nickname, local_nickname_regex())
623 |> validate_format(:email, @email_regex)
624 |> validate_length(:bio, max: bio_limit)
625 |> validate_length(:name, min: 1, max: name_limit)
626 |> maybe_validate_required_email(opts[:external])
627 |> put_password_hash
628 |> put_ap_id()
629 |> unique_constraint(:ap_id)
630 |> put_following_and_follower_address()
631 end
632
633 def maybe_validate_required_email(changeset, true), do: changeset
634
635 def maybe_validate_required_email(changeset, _) do
636 if Pleroma.Config.get([:instance, :account_activation_required]) do
637 validate_required(changeset, [:email])
638 else
639 changeset
640 end
641 end
642
643 defp put_ap_id(changeset) do
644 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
645 put_change(changeset, :ap_id, ap_id)
646 end
647
648 defp put_following_and_follower_address(changeset) do
649 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
650
651 changeset
652 |> put_change(:follower_address, followers)
653 end
654
655 defp autofollow_users(user) do
656 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
657
658 autofollowed_users =
659 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
660 |> Repo.all()
661
662 follow_all(user, autofollowed_users)
663 end
664
665 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
666 def register(%Ecto.Changeset{} = changeset) do
667 with {:ok, user} <- Repo.insert(changeset) do
668 post_register_action(user)
669 end
670 end
671
672 def post_register_action(%User{} = user) do
673 with {:ok, user} <- autofollow_users(user),
674 {:ok, user} <- set_cache(user),
675 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
676 {:ok, _} <- try_send_confirmation_email(user) do
677 {:ok, user}
678 end
679 end
680
681 def try_send_confirmation_email(%User{} = user) do
682 if user.confirmation_pending &&
683 Pleroma.Config.get([:instance, :account_activation_required]) do
684 user
685 |> Pleroma.Emails.UserEmail.account_confirmation_email()
686 |> Pleroma.Emails.Mailer.deliver_async()
687
688 {:ok, :enqueued}
689 else
690 {:ok, :noop}
691 end
692 end
693
694 def try_send_confirmation_email(users) do
695 Enum.each(users, &try_send_confirmation_email/1)
696 end
697
698 def needs_update?(%User{local: true}), do: false
699
700 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
701
702 def needs_update?(%User{local: false} = user) do
703 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
704 end
705
706 def needs_update?(_), do: true
707
708 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
709
710 # "Locked" (self-locked) users demand explicit authorization of follow requests
711 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
712 follow(follower, followed, :follow_pending)
713 end
714
715 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
716 follow(follower, followed)
717 end
718
719 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
720 if not ap_enabled?(followed) do
721 follow(follower, followed)
722 else
723 {:ok, follower}
724 end
725 end
726
727 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
728 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
729 def follow_all(follower, followeds) do
730 followeds
731 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
732 |> Enum.each(&follow(follower, &1, :follow_accept))
733
734 set_cache(follower)
735 end
736
737 defdelegate following(user), to: FollowingRelationship
738
739 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
740 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
741
742 cond do
743 followed.deactivated ->
744 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
745
746 deny_follow_blocked and blocks?(followed, follower) ->
747 {:error, "Could not follow user: #{followed.nickname} blocked you."}
748
749 true ->
750 FollowingRelationship.follow(follower, followed, state)
751
752 {:ok, _} = update_follower_count(followed)
753
754 follower
755 |> update_following_count()
756 |> set_cache()
757 end
758 end
759
760 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
761 {:error, "Not subscribed!"}
762 end
763
764 def unfollow(%User{} = follower, %User{} = followed) do
765 case get_follow_state(follower, followed) do
766 state when state in [:follow_pending, :follow_accept] ->
767 FollowingRelationship.unfollow(follower, followed)
768 {:ok, followed} = update_follower_count(followed)
769
770 {:ok, follower} =
771 follower
772 |> update_following_count()
773 |> set_cache()
774
775 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
776
777 nil ->
778 {:error, "Not subscribed!"}
779 end
780 end
781
782 defdelegate following?(follower, followed), to: FollowingRelationship
783
784 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
785 def get_follow_state(%User{} = follower, %User{} = following) do
786 following_relationship = FollowingRelationship.get(follower, following)
787
788 case {following_relationship, following.local} do
789 {nil, false} ->
790 case Utils.fetch_latest_follow(follower, following) do
791 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
792 FollowingRelationship.state_to_enum(state)
793
794 _ ->
795 nil
796 end
797
798 {%{state: state}, _} ->
799 state
800
801 {nil, _} ->
802 nil
803 end
804 end
805
806 def locked?(%User{} = user) do
807 user.locked || false
808 end
809
810 def get_by_id(id) do
811 Repo.get_by(User, id: id)
812 end
813
814 def get_by_ap_id(ap_id) do
815 Repo.get_by(User, ap_id: ap_id)
816 end
817
818 def get_all_by_ap_id(ap_ids) do
819 from(u in __MODULE__,
820 where: u.ap_id in ^ap_ids
821 )
822 |> Repo.all()
823 end
824
825 def get_all_by_ids(ids) do
826 from(u in __MODULE__, where: u.id in ^ids)
827 |> Repo.all()
828 end
829
830 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
831 # of the ap_id and the domain and tries to get that user
832 def get_by_guessed_nickname(ap_id) do
833 domain = URI.parse(ap_id).host
834 name = List.last(String.split(ap_id, "/"))
835 nickname = "#{name}@#{domain}"
836
837 get_cached_by_nickname(nickname)
838 end
839
840 def set_cache({:ok, user}), do: set_cache(user)
841 def set_cache({:error, err}), do: {:error, err}
842
843 def set_cache(%User{} = user) do
844 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
845 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
846 {:ok, user}
847 end
848
849 def update_and_set_cache(struct, params) do
850 struct
851 |> update_changeset(params)
852 |> update_and_set_cache()
853 end
854
855 def update_and_set_cache(changeset) do
856 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
857 set_cache(user)
858 end
859 end
860
861 def invalidate_cache(user) do
862 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
863 Cachex.del(:user_cache, "nickname:#{user.nickname}")
864 end
865
866 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
867 def get_cached_by_ap_id(ap_id) do
868 key = "ap_id:#{ap_id}"
869
870 with {:ok, nil} <- Cachex.get(:user_cache, key),
871 user when not is_nil(user) <- get_by_ap_id(ap_id),
872 {:ok, true} <- Cachex.put(:user_cache, key, user) do
873 user
874 else
875 {:ok, user} -> user
876 nil -> nil
877 end
878 end
879
880 def get_cached_by_id(id) do
881 key = "id:#{id}"
882
883 ap_id =
884 Cachex.fetch!(:user_cache, key, fn _ ->
885 user = get_by_id(id)
886
887 if user do
888 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
889 {:commit, user.ap_id}
890 else
891 {:ignore, ""}
892 end
893 end)
894
895 get_cached_by_ap_id(ap_id)
896 end
897
898 def get_cached_by_nickname(nickname) do
899 key = "nickname:#{nickname}"
900
901 Cachex.fetch!(:user_cache, key, fn ->
902 case get_or_fetch_by_nickname(nickname) do
903 {:ok, user} -> {:commit, user}
904 {:error, _error} -> {:ignore, nil}
905 end
906 end)
907 end
908
909 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
910 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
911
912 cond do
913 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
914 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
915
916 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
917 get_cached_by_nickname(nickname_or_id)
918
919 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
920 get_cached_by_nickname(nickname_or_id)
921
922 true ->
923 nil
924 end
925 end
926
927 def get_by_nickname(nickname) do
928 Repo.get_by(User, nickname: nickname) ||
929 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
930 Repo.get_by(User, nickname: local_nickname(nickname))
931 end
932 end
933
934 def get_by_email(email), do: Repo.get_by(User, email: email)
935
936 def get_by_nickname_or_email(nickname_or_email) do
937 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
938 end
939
940 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
941
942 def get_or_fetch_by_nickname(nickname) do
943 with %User{} = user <- get_by_nickname(nickname) do
944 {:ok, user}
945 else
946 _e ->
947 with [_nick, _domain] <- String.split(nickname, "@"),
948 {:ok, user} <- fetch_by_nickname(nickname) do
949 {:ok, user}
950 else
951 _e -> {:error, "not found " <> nickname}
952 end
953 end
954 end
955
956 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
957 def get_followers_query(%User{} = user, nil) do
958 User.Query.build(%{followers: user, deactivated: false})
959 end
960
961 def get_followers_query(user, page) do
962 user
963 |> get_followers_query(nil)
964 |> User.Query.paginate(page, 20)
965 end
966
967 @spec get_followers_query(User.t()) :: Ecto.Query.t()
968 def get_followers_query(user), do: get_followers_query(user, nil)
969
970 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
971 def get_followers(user, page \\ nil) do
972 user
973 |> get_followers_query(page)
974 |> Repo.all()
975 end
976
977 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
978 def get_external_followers(user, page \\ nil) do
979 user
980 |> get_followers_query(page)
981 |> User.Query.build(%{external: true})
982 |> Repo.all()
983 end
984
985 def get_followers_ids(user, page \\ nil) do
986 user
987 |> get_followers_query(page)
988 |> select([u], u.id)
989 |> Repo.all()
990 end
991
992 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
993 def get_friends_query(%User{} = user, nil) do
994 User.Query.build(%{friends: user, deactivated: false})
995 end
996
997 def get_friends_query(user, page) do
998 user
999 |> get_friends_query(nil)
1000 |> User.Query.paginate(page, 20)
1001 end
1002
1003 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1004 def get_friends_query(user), do: get_friends_query(user, nil)
1005
1006 def get_friends(user, page \\ nil) do
1007 user
1008 |> get_friends_query(page)
1009 |> Repo.all()
1010 end
1011
1012 def get_friends_ap_ids(user) do
1013 user
1014 |> get_friends_query(nil)
1015 |> select([u], u.ap_id)
1016 |> Repo.all()
1017 end
1018
1019 def get_friends_ids(user, page \\ nil) do
1020 user
1021 |> get_friends_query(page)
1022 |> select([u], u.id)
1023 |> Repo.all()
1024 end
1025
1026 defdelegate get_follow_requests(user), to: FollowingRelationship
1027
1028 def increase_note_count(%User{} = user) do
1029 User
1030 |> where(id: ^user.id)
1031 |> update([u], inc: [note_count: 1])
1032 |> select([u], u)
1033 |> Repo.update_all([])
1034 |> case do
1035 {1, [user]} -> set_cache(user)
1036 _ -> {:error, user}
1037 end
1038 end
1039
1040 def decrease_note_count(%User{} = user) do
1041 User
1042 |> where(id: ^user.id)
1043 |> update([u],
1044 set: [
1045 note_count: fragment("greatest(0, note_count - 1)")
1046 ]
1047 )
1048 |> select([u], u)
1049 |> Repo.update_all([])
1050 |> case do
1051 {1, [user]} -> set_cache(user)
1052 _ -> {:error, user}
1053 end
1054 end
1055
1056 def update_note_count(%User{} = user, note_count \\ nil) do
1057 note_count =
1058 note_count ||
1059 from(
1060 a in Object,
1061 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1062 select: count(a.id)
1063 )
1064 |> Repo.one()
1065
1066 user
1067 |> cast(%{note_count: note_count}, [:note_count])
1068 |> update_and_set_cache()
1069 end
1070
1071 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1072 def maybe_fetch_follow_information(user) do
1073 with {:ok, user} <- fetch_follow_information(user) do
1074 user
1075 else
1076 e ->
1077 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1078
1079 user
1080 end
1081 end
1082
1083 def fetch_follow_information(user) do
1084 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1085 user
1086 |> follow_information_changeset(info)
1087 |> update_and_set_cache()
1088 end
1089 end
1090
1091 defp follow_information_changeset(user, params) do
1092 user
1093 |> cast(params, [
1094 :hide_followers,
1095 :hide_follows,
1096 :follower_count,
1097 :following_count,
1098 :hide_followers_count,
1099 :hide_follows_count
1100 ])
1101 end
1102
1103 def update_follower_count(%User{} = user) do
1104 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1105 follower_count_query =
1106 User.Query.build(%{followers: user, deactivated: false})
1107 |> select([u], %{count: count(u.id)})
1108
1109 User
1110 |> where(id: ^user.id)
1111 |> join(:inner, [u], s in subquery(follower_count_query))
1112 |> update([u, s],
1113 set: [follower_count: s.count]
1114 )
1115 |> select([u], u)
1116 |> Repo.update_all([])
1117 |> case do
1118 {1, [user]} -> set_cache(user)
1119 _ -> {:error, user}
1120 end
1121 else
1122 {:ok, maybe_fetch_follow_information(user)}
1123 end
1124 end
1125
1126 @spec update_following_count(User.t()) :: User.t()
1127 def update_following_count(%User{local: false} = user) do
1128 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1129 maybe_fetch_follow_information(user)
1130 else
1131 user
1132 end
1133 end
1134
1135 def update_following_count(%User{local: true} = user) do
1136 following_count = FollowingRelationship.following_count(user)
1137
1138 user
1139 |> follow_information_changeset(%{following_count: following_count})
1140 |> Repo.update!()
1141 end
1142
1143 def set_unread_conversation_count(%User{local: true} = user) do
1144 unread_query = Participation.unread_conversation_count_for_user(user)
1145
1146 User
1147 |> join(:inner, [u], p in subquery(unread_query))
1148 |> update([u, p],
1149 set: [unread_conversation_count: p.count]
1150 )
1151 |> where([u], u.id == ^user.id)
1152 |> select([u], u)
1153 |> Repo.update_all([])
1154 |> case do
1155 {1, [user]} -> set_cache(user)
1156 _ -> {:error, user}
1157 end
1158 end
1159
1160 def set_unread_conversation_count(user), do: {:ok, user}
1161
1162 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1163 unread_query =
1164 Participation.unread_conversation_count_for_user(user)
1165 |> where([p], p.conversation_id == ^conversation.id)
1166
1167 User
1168 |> join(:inner, [u], p in subquery(unread_query))
1169 |> update([u, p],
1170 inc: [unread_conversation_count: 1]
1171 )
1172 |> where([u], u.id == ^user.id)
1173 |> where([u, p], p.count == 0)
1174 |> select([u], u)
1175 |> Repo.update_all([])
1176 |> case do
1177 {1, [user]} -> set_cache(user)
1178 _ -> {:error, user}
1179 end
1180 end
1181
1182 def increment_unread_conversation_count(_, user), do: {:ok, user}
1183
1184 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1185 def get_users_from_set(ap_ids, local_only \\ true) do
1186 criteria = %{ap_id: ap_ids, deactivated: false}
1187 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1188
1189 User.Query.build(criteria)
1190 |> Repo.all()
1191 end
1192
1193 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1194 def get_recipients_from_activity(%Activity{recipients: to}) do
1195 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1196 |> Repo.all()
1197 end
1198
1199 @spec mute(User.t(), User.t(), boolean()) ::
1200 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1201 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1202 add_to_mutes(muter, mutee, notifications?)
1203 end
1204
1205 def unmute(%User{} = muter, %User{} = mutee) do
1206 remove_from_mutes(muter, mutee)
1207 end
1208
1209 def subscribe(%User{} = subscriber, %User{} = target) do
1210 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1211
1212 if blocks?(target, subscriber) and deny_follow_blocked do
1213 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1214 else
1215 # Note: the relationship is inverse: subscriber acts as relationship target
1216 UserRelationship.create_inverse_subscription(target, subscriber)
1217 end
1218 end
1219
1220 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1221 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1222 subscribe(subscriber, subscribee)
1223 end
1224 end
1225
1226 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1227 # Note: the relationship is inverse: subscriber acts as relationship target
1228 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1229 end
1230
1231 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1232 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1233 unsubscribe(unsubscriber, user)
1234 end
1235 end
1236
1237 def block(%User{} = blocker, %User{} = blocked) do
1238 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1239 blocker =
1240 if following?(blocker, blocked) do
1241 {:ok, blocker, _} = unfollow(blocker, blocked)
1242 blocker
1243 else
1244 blocker
1245 end
1246
1247 # clear any requested follows as well
1248 blocked =
1249 case CommonAPI.reject_follow_request(blocked, blocker) do
1250 {:ok, %User{} = updated_blocked} -> updated_blocked
1251 nil -> blocked
1252 end
1253
1254 unsubscribe(blocked, blocker)
1255
1256 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1257
1258 {:ok, blocker} = update_follower_count(blocker)
1259 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1260 add_to_block(blocker, blocked)
1261 end
1262
1263 # helper to handle the block given only an actor's AP id
1264 def block(%User{} = blocker, %{ap_id: ap_id}) do
1265 block(blocker, get_cached_by_ap_id(ap_id))
1266 end
1267
1268 def unblock(%User{} = blocker, %User{} = blocked) do
1269 remove_from_block(blocker, blocked)
1270 end
1271
1272 # helper to handle the block given only an actor's AP id
1273 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1274 unblock(blocker, get_cached_by_ap_id(ap_id))
1275 end
1276
1277 def mutes?(nil, _), do: false
1278 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1279
1280 def mutes_user?(%User{} = user, %User{} = target) do
1281 UserRelationship.mute_exists?(user, target)
1282 end
1283
1284 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1285 def muted_notifications?(nil, _), do: false
1286
1287 def muted_notifications?(%User{} = user, %User{} = target),
1288 do: UserRelationship.notification_mute_exists?(user, target)
1289
1290 def blocks?(nil, _), do: false
1291
1292 def blocks?(%User{} = user, %User{} = target) do
1293 blocks_user?(user, target) ||
1294 (blocks_domain?(user, target) and not User.following?(user, target))
1295 end
1296
1297 def blocks_user?(%User{} = user, %User{} = target) do
1298 UserRelationship.block_exists?(user, target)
1299 end
1300
1301 def blocks_user?(_, _), do: false
1302
1303 def blocks_domain?(%User{} = user, %User{} = target) do
1304 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1305 %{host: host} = URI.parse(target.ap_id)
1306 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1307 end
1308
1309 def blocks_domain?(_, _), do: false
1310
1311 def subscribed_to?(%User{} = user, %User{} = target) do
1312 # Note: the relationship is inverse: subscriber acts as relationship target
1313 UserRelationship.inverse_subscription_exists?(target, user)
1314 end
1315
1316 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1317 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1318 subscribed_to?(user, target)
1319 end
1320 end
1321
1322 @doc """
1323 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1324 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1325 """
1326 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1327 def outgoing_relationships_ap_ids(_user, []), do: %{}
1328
1329 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1330
1331 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1332 when is_list(relationship_types) do
1333 db_result =
1334 user
1335 |> assoc(:outgoing_relationships)
1336 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1337 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1338 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1339 |> group_by([user_rel, u], user_rel.relationship_type)
1340 |> Repo.all()
1341 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1342
1343 Enum.into(
1344 relationship_types,
1345 %{},
1346 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1347 )
1348 end
1349
1350 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1351
1352 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1353
1354 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1355
1356 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1357 when is_list(relationship_types) do
1358 user
1359 |> assoc(:incoming_relationships)
1360 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1361 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1362 |> maybe_filter_on_ap_id(ap_ids)
1363 |> select([user_rel, u], u.ap_id)
1364 |> distinct(true)
1365 |> Repo.all()
1366 end
1367
1368 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1369 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1370 end
1371
1372 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1373
1374 def deactivate_async(user, status \\ true) do
1375 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1376 end
1377
1378 def deactivate(user, status \\ true)
1379
1380 def deactivate(users, status) when is_list(users) do
1381 Repo.transaction(fn ->
1382 for user <- users, do: deactivate(user, status)
1383 end)
1384 end
1385
1386 def deactivate(%User{} = user, status) do
1387 with {:ok, user} <- set_activation_status(user, status) do
1388 user
1389 |> get_followers()
1390 |> Enum.filter(& &1.local)
1391 |> Enum.each(fn follower ->
1392 follower |> update_following_count() |> set_cache()
1393 end)
1394
1395 # Only update local user counts, remote will be update during the next pull.
1396 user
1397 |> get_friends()
1398 |> Enum.filter(& &1.local)
1399 |> Enum.each(&update_follower_count/1)
1400
1401 {:ok, user}
1402 end
1403 end
1404
1405 def update_notification_settings(%User{} = user, settings) do
1406 user
1407 |> cast(%{notification_settings: settings}, [])
1408 |> cast_embed(:notification_settings)
1409 |> validate_required([:notification_settings])
1410 |> update_and_set_cache()
1411 end
1412
1413 def delete(users) when is_list(users) do
1414 for user <- users, do: delete(user)
1415 end
1416
1417 def delete(%User{} = user) do
1418 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1419 end
1420
1421 def perform(:force_password_reset, user), do: force_password_reset(user)
1422
1423 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1424 def perform(:delete, %User{} = user) do
1425 {:ok, _user} = ActivityPub.delete(user)
1426
1427 # Remove all relationships
1428 user
1429 |> get_followers()
1430 |> Enum.each(fn follower ->
1431 ActivityPub.unfollow(follower, user)
1432 unfollow(follower, user)
1433 end)
1434
1435 user
1436 |> get_friends()
1437 |> Enum.each(fn followed ->
1438 ActivityPub.unfollow(user, followed)
1439 unfollow(user, followed)
1440 end)
1441
1442 delete_user_activities(user)
1443
1444 if user.local do
1445 user
1446 |> change(%{deactivated: true, email: nil})
1447 |> update_and_set_cache()
1448 else
1449 invalidate_cache(user)
1450 Repo.delete(user)
1451 end
1452 end
1453
1454 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1455
1456 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1457 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1458 when is_list(blocked_identifiers) do
1459 Enum.map(
1460 blocked_identifiers,
1461 fn blocked_identifier ->
1462 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1463 {:ok, _user_block} <- block(blocker, blocked),
1464 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1465 blocked
1466 else
1467 err ->
1468 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1469 err
1470 end
1471 end
1472 )
1473 end
1474
1475 def perform(:follow_import, %User{} = follower, followed_identifiers)
1476 when is_list(followed_identifiers) do
1477 Enum.map(
1478 followed_identifiers,
1479 fn followed_identifier ->
1480 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1481 {:ok, follower} <- maybe_direct_follow(follower, followed),
1482 {:ok, _} <- ActivityPub.follow(follower, followed) do
1483 followed
1484 else
1485 err ->
1486 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1487 err
1488 end
1489 end
1490 )
1491 end
1492
1493 @spec external_users_query() :: Ecto.Query.t()
1494 def external_users_query do
1495 User.Query.build(%{
1496 external: true,
1497 active: true,
1498 order_by: :id
1499 })
1500 end
1501
1502 @spec external_users(keyword()) :: [User.t()]
1503 def external_users(opts \\ []) do
1504 query =
1505 external_users_query()
1506 |> select([u], struct(u, [:id, :ap_id]))
1507
1508 query =
1509 if opts[:max_id],
1510 do: where(query, [u], u.id > ^opts[:max_id]),
1511 else: query
1512
1513 query =
1514 if opts[:limit],
1515 do: limit(query, ^opts[:limit]),
1516 else: query
1517
1518 Repo.all(query)
1519 end
1520
1521 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1522 BackgroundWorker.enqueue("blocks_import", %{
1523 "blocker_id" => blocker.id,
1524 "blocked_identifiers" => blocked_identifiers
1525 })
1526 end
1527
1528 def follow_import(%User{} = follower, followed_identifiers)
1529 when is_list(followed_identifiers) do
1530 BackgroundWorker.enqueue("follow_import", %{
1531 "follower_id" => follower.id,
1532 "followed_identifiers" => followed_identifiers
1533 })
1534 end
1535
1536 def delete_user_activities(%User{ap_id: ap_id}) do
1537 ap_id
1538 |> Activity.Queries.by_actor()
1539 |> RepoStreamer.chunk_stream(50)
1540 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1541 |> Stream.run()
1542 end
1543
1544 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1545 activity
1546 |> Object.normalize()
1547 |> ActivityPub.delete()
1548 end
1549
1550 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1551 object = Object.normalize(activity)
1552
1553 activity.actor
1554 |> get_cached_by_ap_id()
1555 |> ActivityPub.unlike(object)
1556 end
1557
1558 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1559 object = Object.normalize(activity)
1560
1561 activity.actor
1562 |> get_cached_by_ap_id()
1563 |> ActivityPub.unannounce(object)
1564 end
1565
1566 defp delete_activity(_activity), do: "Doing nothing"
1567
1568 def html_filter_policy(%User{no_rich_text: true}) do
1569 Pleroma.HTML.Scrubber.TwitterText
1570 end
1571
1572 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1573
1574 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1575
1576 def get_or_fetch_by_ap_id(ap_id) do
1577 user = get_cached_by_ap_id(ap_id)
1578
1579 if !is_nil(user) and !needs_update?(user) do
1580 {:ok, user}
1581 else
1582 fetch_by_ap_id(ap_id)
1583 end
1584 end
1585
1586 @doc """
1587 Creates an internal service actor by URI if missing.
1588 Optionally takes nickname for addressing.
1589 """
1590 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1591 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1592 {_, user} =
1593 case get_cached_by_ap_id(uri) do
1594 nil ->
1595 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1596 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1597 {:error, nil}
1598 end
1599
1600 %User{invisible: false} = user ->
1601 set_invisible(user)
1602
1603 user ->
1604 {:ok, user}
1605 end
1606
1607 user
1608 end
1609
1610 @spec set_invisible(User.t()) :: {:ok, User.t()}
1611 defp set_invisible(user) do
1612 user
1613 |> change(%{invisible: true})
1614 |> update_and_set_cache()
1615 end
1616
1617 @spec create_service_actor(String.t(), String.t()) ::
1618 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1619 defp create_service_actor(uri, nickname) do
1620 %User{
1621 invisible: true,
1622 local: true,
1623 ap_id: uri,
1624 nickname: nickname,
1625 follower_address: uri <> "/followers"
1626 }
1627 |> change
1628 |> unique_constraint(:nickname)
1629 |> Repo.insert()
1630 |> set_cache()
1631 end
1632
1633 # AP style
1634 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1635 key =
1636 public_key_pem
1637 |> :public_key.pem_decode()
1638 |> hd()
1639 |> :public_key.pem_entry_decode()
1640
1641 {:ok, key}
1642 end
1643
1644 def public_key(_), do: {:error, "not found key"}
1645
1646 def get_public_key_for_ap_id(ap_id) do
1647 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1648 {:ok, public_key} <- public_key(user) do
1649 {:ok, public_key}
1650 else
1651 _ -> :error
1652 end
1653 end
1654
1655 defp blank?(""), do: nil
1656 defp blank?(n), do: n
1657
1658 def insert_or_update_user(data) do
1659 data
1660 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1661 |> remote_user_creation()
1662 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1663 |> set_cache()
1664 end
1665
1666 def ap_enabled?(%User{local: true}), do: true
1667 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1668 def ap_enabled?(_), do: false
1669
1670 @doc "Gets or fetch a user by uri or nickname."
1671 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1672 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1673 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1674
1675 # wait a period of time and return newest version of the User structs
1676 # this is because we have synchronous follow APIs and need to simulate them
1677 # with an async handshake
1678 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1679 with %User{} = a <- get_cached_by_id(a.id),
1680 %User{} = b <- get_cached_by_id(b.id) do
1681 {:ok, a, b}
1682 else
1683 nil -> :error
1684 end
1685 end
1686
1687 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1688 with :ok <- :timer.sleep(timeout),
1689 %User{} = a <- get_cached_by_id(a.id),
1690 %User{} = b <- get_cached_by_id(b.id) do
1691 {:ok, a, b}
1692 else
1693 nil -> :error
1694 end
1695 end
1696
1697 def parse_bio(bio) when is_binary(bio) and bio != "" do
1698 bio
1699 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1700 |> elem(0)
1701 end
1702
1703 def parse_bio(_), do: ""
1704
1705 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1706 # TODO: get profile URLs other than user.ap_id
1707 profile_urls = [user.ap_id]
1708
1709 bio
1710 |> CommonUtils.format_input("text/plain",
1711 mentions_format: :full,
1712 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1713 )
1714 |> elem(0)
1715 end
1716
1717 def parse_bio(_, _), do: ""
1718
1719 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1720 Repo.transaction(fn ->
1721 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1722 end)
1723 end
1724
1725 def tag(nickname, tags) when is_binary(nickname),
1726 do: tag(get_by_nickname(nickname), tags)
1727
1728 def tag(%User{} = user, tags),
1729 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1730
1731 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1732 Repo.transaction(fn ->
1733 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1734 end)
1735 end
1736
1737 def untag(nickname, tags) when is_binary(nickname),
1738 do: untag(get_by_nickname(nickname), tags)
1739
1740 def untag(%User{} = user, tags),
1741 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1742
1743 defp update_tags(%User{} = user, new_tags) do
1744 {:ok, updated_user} =
1745 user
1746 |> change(%{tags: new_tags})
1747 |> update_and_set_cache()
1748
1749 updated_user
1750 end
1751
1752 defp normalize_tags(tags) do
1753 [tags]
1754 |> List.flatten()
1755 |> Enum.map(&String.downcase/1)
1756 end
1757
1758 defp local_nickname_regex do
1759 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1760 @extended_local_nickname_regex
1761 else
1762 @strict_local_nickname_regex
1763 end
1764 end
1765
1766 def local_nickname(nickname_or_mention) do
1767 nickname_or_mention
1768 |> full_nickname()
1769 |> String.split("@")
1770 |> hd()
1771 end
1772
1773 def full_nickname(nickname_or_mention),
1774 do: String.trim_leading(nickname_or_mention, "@")
1775
1776 def error_user(ap_id) do
1777 %User{
1778 name: ap_id,
1779 ap_id: ap_id,
1780 nickname: "erroruser@example.com",
1781 inserted_at: NaiveDateTime.utc_now()
1782 }
1783 end
1784
1785 @spec all_superusers() :: [User.t()]
1786 def all_superusers do
1787 User.Query.build(%{super_users: true, local: true, deactivated: false})
1788 |> Repo.all()
1789 end
1790
1791 def showing_reblogs?(%User{} = user, %User{} = target) do
1792 not UserRelationship.reblog_mute_exists?(user, target)
1793 end
1794
1795 @doc """
1796 The function returns a query to get users with no activity for given interval of days.
1797 Inactive users are those who didn't read any notification, or had any activity where
1798 the user is the activity's actor, during `inactivity_threshold` days.
1799 Deactivated users will not appear in this list.
1800
1801 ## Examples
1802
1803 iex> Pleroma.User.list_inactive_users()
1804 %Ecto.Query{}
1805 """
1806 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1807 def list_inactive_users_query(inactivity_threshold \\ 7) do
1808 negative_inactivity_threshold = -inactivity_threshold
1809 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1810 # Subqueries are not supported in `where` clauses, join gets too complicated.
1811 has_read_notifications =
1812 from(n in Pleroma.Notification,
1813 where: n.seen == true,
1814 group_by: n.id,
1815 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1816 select: n.user_id
1817 )
1818 |> Pleroma.Repo.all()
1819
1820 from(u in Pleroma.User,
1821 left_join: a in Pleroma.Activity,
1822 on: u.ap_id == a.actor,
1823 where: not is_nil(u.nickname),
1824 where: u.deactivated != ^true,
1825 where: u.id not in ^has_read_notifications,
1826 group_by: u.id,
1827 having:
1828 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1829 is_nil(max(a.inserted_at))
1830 )
1831 end
1832
1833 @doc """
1834 Enable or disable email notifications for user
1835
1836 ## Examples
1837
1838 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1839 Pleroma.User{email_notifications: %{"digest" => true}}
1840
1841 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1842 Pleroma.User{email_notifications: %{"digest" => false}}
1843 """
1844 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1845 {:ok, t()} | {:error, Ecto.Changeset.t()}
1846 def switch_email_notifications(user, type, status) do
1847 User.update_email_notifications(user, %{type => status})
1848 end
1849
1850 @doc """
1851 Set `last_digest_emailed_at` value for the user to current time
1852 """
1853 @spec touch_last_digest_emailed_at(t()) :: t()
1854 def touch_last_digest_emailed_at(user) do
1855 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1856
1857 {:ok, updated_user} =
1858 user
1859 |> change(%{last_digest_emailed_at: now})
1860 |> update_and_set_cache()
1861
1862 updated_user
1863 end
1864
1865 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1866 def toggle_confirmation(%User{} = user) do
1867 user
1868 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1869 |> update_and_set_cache()
1870 end
1871
1872 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1873 def toggle_confirmation(users) do
1874 Enum.map(users, &toggle_confirmation/1)
1875 end
1876
1877 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1878 mascot
1879 end
1880
1881 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1882 # use instance-default
1883 config = Pleroma.Config.get([:assets, :mascots])
1884 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1885 mascot = Keyword.get(config, default_mascot)
1886
1887 %{
1888 "id" => "default-mascot",
1889 "url" => mascot[:url],
1890 "preview_url" => mascot[:url],
1891 "pleroma" => %{
1892 "mime_type" => mascot[:mime_type]
1893 }
1894 }
1895 end
1896
1897 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1898
1899 def ensure_keys_present(%User{} = user) do
1900 with {:ok, pem} <- Keys.generate_rsa_pem() do
1901 user
1902 |> cast(%{keys: pem}, [:keys])
1903 |> validate_required([:keys])
1904 |> update_and_set_cache()
1905 end
1906 end
1907
1908 def get_ap_ids_by_nicknames(nicknames) do
1909 from(u in User,
1910 where: u.nickname in ^nicknames,
1911 select: u.ap_id
1912 )
1913 |> Repo.all()
1914 end
1915
1916 defdelegate search(query, opts \\ []), to: User.Search
1917
1918 defp put_password_hash(
1919 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1920 ) do
1921 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1922 end
1923
1924 defp put_password_hash(changeset), do: changeset
1925
1926 def is_internal_user?(%User{nickname: nil}), do: true
1927 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1928 def is_internal_user?(_), do: false
1929
1930 # A hack because user delete activities have a fake id for whatever reason
1931 # TODO: Get rid of this
1932 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1933
1934 def get_delivered_users_by_object_id(object_id) do
1935 from(u in User,
1936 inner_join: delivery in assoc(u, :deliveries),
1937 where: delivery.object_id == ^object_id
1938 )
1939 |> Repo.all()
1940 end
1941
1942 def change_email(user, email) do
1943 user
1944 |> cast(%{email: email}, [:email])
1945 |> validate_required([:email])
1946 |> unique_constraint(:email)
1947 |> validate_format(:email, @email_regex)
1948 |> update_and_set_cache()
1949 end
1950
1951 # Internal function; public one is `deactivate/2`
1952 defp set_activation_status(user, deactivated) do
1953 user
1954 |> cast(%{deactivated: deactivated}, [:deactivated])
1955 |> update_and_set_cache()
1956 end
1957
1958 def update_banner(user, banner) do
1959 user
1960 |> cast(%{banner: banner}, [:banner])
1961 |> update_and_set_cache()
1962 end
1963
1964 def update_background(user, background) do
1965 user
1966 |> cast(%{background: background}, [:background])
1967 |> update_and_set_cache()
1968 end
1969
1970 def update_source_data(user, source_data) do
1971 user
1972 |> cast(%{source_data: source_data}, [:source_data])
1973 |> update_and_set_cache()
1974 end
1975
1976 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1977 %{
1978 admin: is_admin,
1979 moderator: is_moderator
1980 }
1981 end
1982
1983 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1984 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1985 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1986 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1987
1988 attachment
1989 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1990 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1991 |> Enum.take(limit)
1992 end
1993
1994 def fields(%{fields: nil}), do: []
1995
1996 def fields(%{fields: fields}), do: fields
1997
1998 def validate_fields(changeset, remote? \\ false) do
1999 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2000 limit = Pleroma.Config.get([:instance, limit_name], 0)
2001
2002 changeset
2003 |> validate_length(:fields, max: limit)
2004 |> validate_change(:fields, fn :fields, fields ->
2005 if Enum.all?(fields, &valid_field?/1) do
2006 []
2007 else
2008 [fields: "invalid"]
2009 end
2010 end)
2011 end
2012
2013 defp valid_field?(%{"name" => name, "value" => value}) do
2014 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2015 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2016
2017 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2018 String.length(value) <= value_limit
2019 end
2020
2021 defp valid_field?(_), do: false
2022
2023 defp truncate_field(%{"name" => name, "value" => value}) do
2024 {name, _chopped} =
2025 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2026
2027 {value, _chopped} =
2028 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2029
2030 %{"name" => name, "value" => value}
2031 end
2032
2033 def admin_api_update(user, params) do
2034 user
2035 |> cast(params, [
2036 :is_moderator,
2037 :is_admin,
2038 :show_role
2039 ])
2040 |> update_and_set_cache()
2041 end
2042
2043 @doc "Signs user out of all applications"
2044 def global_sign_out(user) do
2045 OAuth.Authorization.delete_user_authorizations(user)
2046 OAuth.Token.delete_user_tokens(user)
2047 end
2048
2049 def mascot_update(user, url) do
2050 user
2051 |> cast(%{mascot: url}, [:mascot])
2052 |> validate_required([:mascot])
2053 |> update_and_set_cache()
2054 end
2055
2056 def mastodon_settings_update(user, settings) do
2057 user
2058 |> cast(%{settings: settings}, [:settings])
2059 |> validate_required([:settings])
2060 |> update_and_set_cache()
2061 end
2062
2063 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2064 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2065 params =
2066 if need_confirmation? do
2067 %{
2068 confirmation_pending: true,
2069 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2070 }
2071 else
2072 %{
2073 confirmation_pending: false,
2074 confirmation_token: nil
2075 }
2076 end
2077
2078 cast(user, params, [:confirmation_pending, :confirmation_token])
2079 end
2080
2081 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2082 if id not in user.pinned_activities do
2083 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2084 params = %{pinned_activities: user.pinned_activities ++ [id]}
2085
2086 user
2087 |> cast(params, [:pinned_activities])
2088 |> validate_length(:pinned_activities,
2089 max: max_pinned_statuses,
2090 message: "You have already pinned the maximum number of statuses"
2091 )
2092 else
2093 change(user)
2094 end
2095 |> update_and_set_cache()
2096 end
2097
2098 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2099 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2100
2101 user
2102 |> cast(params, [:pinned_activities])
2103 |> update_and_set_cache()
2104 end
2105
2106 def update_email_notifications(user, settings) do
2107 email_notifications =
2108 user.email_notifications
2109 |> Map.merge(settings)
2110 |> Map.take(["digest"])
2111
2112 params = %{email_notifications: email_notifications}
2113 fields = [:email_notifications]
2114
2115 user
2116 |> cast(params, fields)
2117 |> validate_required(fields)
2118 |> update_and_set_cache()
2119 end
2120
2121 defp set_domain_blocks(user, domain_blocks) do
2122 params = %{domain_blocks: domain_blocks}
2123
2124 user
2125 |> cast(params, [:domain_blocks])
2126 |> validate_required([:domain_blocks])
2127 |> update_and_set_cache()
2128 end
2129
2130 def block_domain(user, domain_blocked) do
2131 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2132 end
2133
2134 def unblock_domain(user, domain_blocked) do
2135 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2136 end
2137
2138 @spec add_to_block(User.t(), User.t()) ::
2139 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2140 defp add_to_block(%User{} = user, %User{} = blocked) do
2141 UserRelationship.create_block(user, blocked)
2142 end
2143
2144 @spec add_to_block(User.t(), User.t()) ::
2145 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2146 defp remove_from_block(%User{} = user, %User{} = blocked) do
2147 UserRelationship.delete_block(user, blocked)
2148 end
2149
2150 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2151 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2152 {:ok, user_notification_mute} <-
2153 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2154 {:ok, nil} do
2155 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2156 end
2157 end
2158
2159 defp remove_from_mutes(user, %User{} = muted_user) do
2160 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2161 {:ok, user_notification_mute} <-
2162 UserRelationship.delete_notification_mute(user, muted_user) do
2163 {:ok, [user_mute, user_notification_mute]}
2164 end
2165 end
2166
2167 def set_invisible(user, invisible) do
2168 params = %{invisible: invisible}
2169
2170 user
2171 |> cast(params, [:invisible])
2172 |> validate_required([:invisible])
2173 |> update_and_set_cache()
2174 end
2175
2176 def sanitize_html(%User{} = user) do
2177 sanitize_html(user, nil)
2178 end
2179
2180 # User data that mastodon isn't filtering (treated as plaintext):
2181 # - field name
2182 # - display name
2183 def sanitize_html(%User{} = user, filter) do
2184 fields =
2185 user
2186 |> User.fields()
2187 |> Enum.map(fn %{"name" => name, "value" => value} ->
2188 %{
2189 "name" => name,
2190 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2191 }
2192 end)
2193
2194 user
2195 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2196 |> Map.put(:fields, fields)
2197 end
2198 end