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