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