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