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