4f3abd7d5c521cb258e79758eba6fe8e4e8914d7
[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, :follow_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, :follow_accept))
721
722 set_cache(follower)
723 end
724
725 defdelegate following(user), to: FollowingRelationship
726
727 def follow(%User{} = follower, %User{} = followed, state \\ :follow_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 [:follow_pending, :follow_accept] ->
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 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
773 def get_follow_state(%User{} = follower, %User{} = following) do
774 following_relationship = FollowingRelationship.get(follower, following)
775 get_follow_state(follower, following, following_relationship)
776 end
777
778 def get_follow_state(
779 %User{} = follower,
780 %User{} = following,
781 following_relationship
782 ) do
783 case {following_relationship, following.local} do
784 {nil, false} ->
785 case Utils.fetch_latest_follow(follower, following) do
786 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
787 FollowingRelationship.state_to_enum(state)
788
789 _ ->
790 nil
791 end
792
793 {%{state: state}, _} ->
794 state
795
796 {nil, _} ->
797 nil
798 end
799 end
800
801 def locked?(%User{} = user) do
802 user.locked || false
803 end
804
805 def get_by_id(id) do
806 Repo.get_by(User, id: id)
807 end
808
809 def get_by_ap_id(ap_id) do
810 Repo.get_by(User, ap_id: ap_id)
811 end
812
813 def get_all_by_ap_id(ap_ids) do
814 from(u in __MODULE__,
815 where: u.ap_id in ^ap_ids
816 )
817 |> Repo.all()
818 end
819
820 def get_all_by_ids(ids) do
821 from(u in __MODULE__, where: u.id in ^ids)
822 |> Repo.all()
823 end
824
825 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
826 # of the ap_id and the domain and tries to get that user
827 def get_by_guessed_nickname(ap_id) do
828 domain = URI.parse(ap_id).host
829 name = List.last(String.split(ap_id, "/"))
830 nickname = "#{name}@#{domain}"
831
832 get_cached_by_nickname(nickname)
833 end
834
835 def set_cache({:ok, user}), do: set_cache(user)
836 def set_cache({:error, err}), do: {:error, err}
837
838 def set_cache(%User{} = user) do
839 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
840 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
841 {:ok, user}
842 end
843
844 def update_and_set_cache(struct, params) do
845 struct
846 |> update_changeset(params)
847 |> update_and_set_cache()
848 end
849
850 def update_and_set_cache(changeset) do
851 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
852 set_cache(user)
853 end
854 end
855
856 def invalidate_cache(user) do
857 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
858 Cachex.del(:user_cache, "nickname:#{user.nickname}")
859 end
860
861 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
862 def get_cached_by_ap_id(ap_id) do
863 key = "ap_id:#{ap_id}"
864
865 with {:ok, nil} <- Cachex.get(:user_cache, key),
866 user when not is_nil(user) <- get_by_ap_id(ap_id),
867 {:ok, true} <- Cachex.put(:user_cache, key, user) do
868 user
869 else
870 {:ok, user} -> user
871 nil -> nil
872 end
873 end
874
875 def get_cached_by_id(id) do
876 key = "id:#{id}"
877
878 ap_id =
879 Cachex.fetch!(:user_cache, key, fn _ ->
880 user = get_by_id(id)
881
882 if user do
883 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
884 {:commit, user.ap_id}
885 else
886 {:ignore, ""}
887 end
888 end)
889
890 get_cached_by_ap_id(ap_id)
891 end
892
893 def get_cached_by_nickname(nickname) do
894 key = "nickname:#{nickname}"
895
896 Cachex.fetch!(:user_cache, key, fn ->
897 case get_or_fetch_by_nickname(nickname) do
898 {:ok, user} -> {:commit, user}
899 {:error, _error} -> {:ignore, nil}
900 end
901 end)
902 end
903
904 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
905 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
906
907 cond do
908 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
909 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
910
911 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
912 get_cached_by_nickname(nickname_or_id)
913
914 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
915 get_cached_by_nickname(nickname_or_id)
916
917 true ->
918 nil
919 end
920 end
921
922 def get_by_nickname(nickname) do
923 Repo.get_by(User, nickname: nickname) ||
924 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
925 Repo.get_by(User, nickname: local_nickname(nickname))
926 end
927 end
928
929 def get_by_email(email), do: Repo.get_by(User, email: email)
930
931 def get_by_nickname_or_email(nickname_or_email) do
932 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
933 end
934
935 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
936
937 def get_or_fetch_by_nickname(nickname) do
938 with %User{} = user <- get_by_nickname(nickname) do
939 {:ok, user}
940 else
941 _e ->
942 with [_nick, _domain] <- String.split(nickname, "@"),
943 {:ok, user} <- fetch_by_nickname(nickname) do
944 {:ok, user}
945 else
946 _e -> {:error, "not found " <> nickname}
947 end
948 end
949 end
950
951 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
952 def get_followers_query(%User{} = user, nil) do
953 User.Query.build(%{followers: user, deactivated: false})
954 end
955
956 def get_followers_query(user, page) do
957 user
958 |> get_followers_query(nil)
959 |> User.Query.paginate(page, 20)
960 end
961
962 @spec get_followers_query(User.t()) :: Ecto.Query.t()
963 def get_followers_query(user), do: get_followers_query(user, nil)
964
965 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
966 def get_followers(user, page \\ nil) do
967 user
968 |> get_followers_query(page)
969 |> Repo.all()
970 end
971
972 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
973 def get_external_followers(user, page \\ nil) do
974 user
975 |> get_followers_query(page)
976 |> User.Query.build(%{external: true})
977 |> Repo.all()
978 end
979
980 def get_followers_ids(user, page \\ nil) do
981 user
982 |> get_followers_query(page)
983 |> select([u], u.id)
984 |> Repo.all()
985 end
986
987 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
988 def get_friends_query(%User{} = user, nil) do
989 User.Query.build(%{friends: user, deactivated: false})
990 end
991
992 def get_friends_query(user, page) do
993 user
994 |> get_friends_query(nil)
995 |> User.Query.paginate(page, 20)
996 end
997
998 @spec get_friends_query(User.t()) :: Ecto.Query.t()
999 def get_friends_query(user), do: get_friends_query(user, nil)
1000
1001 def get_friends(user, page \\ nil) do
1002 user
1003 |> get_friends_query(page)
1004 |> Repo.all()
1005 end
1006
1007 def get_friends_ap_ids(user) do
1008 user
1009 |> get_friends_query(nil)
1010 |> select([u], u.ap_id)
1011 |> Repo.all()
1012 end
1013
1014 def get_friends_ids(user, page \\ nil) do
1015 user
1016 |> get_friends_query(page)
1017 |> select([u], u.id)
1018 |> Repo.all()
1019 end
1020
1021 defdelegate get_follow_requests(user), to: FollowingRelationship
1022
1023 def increase_note_count(%User{} = user) do
1024 User
1025 |> where(id: ^user.id)
1026 |> update([u], inc: [note_count: 1])
1027 |> select([u], u)
1028 |> Repo.update_all([])
1029 |> case do
1030 {1, [user]} -> set_cache(user)
1031 _ -> {:error, user}
1032 end
1033 end
1034
1035 def decrease_note_count(%User{} = user) do
1036 User
1037 |> where(id: ^user.id)
1038 |> update([u],
1039 set: [
1040 note_count: fragment("greatest(0, note_count - 1)")
1041 ]
1042 )
1043 |> select([u], u)
1044 |> Repo.update_all([])
1045 |> case do
1046 {1, [user]} -> set_cache(user)
1047 _ -> {:error, user}
1048 end
1049 end
1050
1051 def update_note_count(%User{} = user, note_count \\ nil) do
1052 note_count =
1053 note_count ||
1054 from(
1055 a in Object,
1056 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1057 select: count(a.id)
1058 )
1059 |> Repo.one()
1060
1061 user
1062 |> cast(%{note_count: note_count}, [:note_count])
1063 |> update_and_set_cache()
1064 end
1065
1066 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1067 def maybe_fetch_follow_information(user) do
1068 with {:ok, user} <- fetch_follow_information(user) do
1069 user
1070 else
1071 e ->
1072 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1073
1074 user
1075 end
1076 end
1077
1078 def fetch_follow_information(user) do
1079 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1080 user
1081 |> follow_information_changeset(info)
1082 |> update_and_set_cache()
1083 end
1084 end
1085
1086 defp follow_information_changeset(user, params) do
1087 user
1088 |> cast(params, [
1089 :hide_followers,
1090 :hide_follows,
1091 :follower_count,
1092 :following_count,
1093 :hide_followers_count,
1094 :hide_follows_count
1095 ])
1096 end
1097
1098 def update_follower_count(%User{} = user) do
1099 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1100 follower_count_query =
1101 User.Query.build(%{followers: user, deactivated: false})
1102 |> select([u], %{count: count(u.id)})
1103
1104 User
1105 |> where(id: ^user.id)
1106 |> join(:inner, [u], s in subquery(follower_count_query))
1107 |> update([u, s],
1108 set: [follower_count: s.count]
1109 )
1110 |> select([u], u)
1111 |> Repo.update_all([])
1112 |> case do
1113 {1, [user]} -> set_cache(user)
1114 _ -> {:error, user}
1115 end
1116 else
1117 {:ok, maybe_fetch_follow_information(user)}
1118 end
1119 end
1120
1121 @spec update_following_count(User.t()) :: User.t()
1122 def update_following_count(%User{local: false} = user) do
1123 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1124 maybe_fetch_follow_information(user)
1125 else
1126 user
1127 end
1128 end
1129
1130 def update_following_count(%User{local: true} = user) do
1131 following_count = FollowingRelationship.following_count(user)
1132
1133 user
1134 |> follow_information_changeset(%{following_count: following_count})
1135 |> Repo.update!()
1136 end
1137
1138 def set_unread_conversation_count(%User{local: true} = user) do
1139 unread_query = Participation.unread_conversation_count_for_user(user)
1140
1141 User
1142 |> join(:inner, [u], p in subquery(unread_query))
1143 |> update([u, p],
1144 set: [unread_conversation_count: p.count]
1145 )
1146 |> where([u], u.id == ^user.id)
1147 |> select([u], u)
1148 |> Repo.update_all([])
1149 |> case do
1150 {1, [user]} -> set_cache(user)
1151 _ -> {:error, user}
1152 end
1153 end
1154
1155 def set_unread_conversation_count(user), do: {:ok, user}
1156
1157 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1158 unread_query =
1159 Participation.unread_conversation_count_for_user(user)
1160 |> where([p], p.conversation_id == ^conversation.id)
1161
1162 User
1163 |> join(:inner, [u], p in subquery(unread_query))
1164 |> update([u, p],
1165 inc: [unread_conversation_count: 1]
1166 )
1167 |> where([u], u.id == ^user.id)
1168 |> where([u, p], p.count == 0)
1169 |> select([u], u)
1170 |> Repo.update_all([])
1171 |> case do
1172 {1, [user]} -> set_cache(user)
1173 _ -> {:error, user}
1174 end
1175 end
1176
1177 def increment_unread_conversation_count(_, user), do: {:ok, user}
1178
1179 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1180 def get_users_from_set(ap_ids, local_only \\ true) do
1181 criteria = %{ap_id: ap_ids, deactivated: false}
1182 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1183
1184 User.Query.build(criteria)
1185 |> Repo.all()
1186 end
1187
1188 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1189 def get_recipients_from_activity(%Activity{recipients: to}) do
1190 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1191 |> Repo.all()
1192 end
1193
1194 @spec mute(User.t(), User.t(), boolean()) ::
1195 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1196 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1197 add_to_mutes(muter, mutee, notifications?)
1198 end
1199
1200 def unmute(%User{} = muter, %User{} = mutee) do
1201 remove_from_mutes(muter, mutee)
1202 end
1203
1204 def subscribe(%User{} = subscriber, %User{} = target) do
1205 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1206
1207 if blocks?(target, subscriber) and deny_follow_blocked do
1208 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1209 else
1210 # Note: the relationship is inverse: subscriber acts as relationship target
1211 UserRelationship.create_inverse_subscription(target, subscriber)
1212 end
1213 end
1214
1215 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1216 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1217 subscribe(subscriber, subscribee)
1218 end
1219 end
1220
1221 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1222 # Note: the relationship is inverse: subscriber acts as relationship target
1223 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1224 end
1225
1226 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1227 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1228 unsubscribe(unsubscriber, user)
1229 end
1230 end
1231
1232 def block(%User{} = blocker, %User{} = blocked) do
1233 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1234 blocker =
1235 if following?(blocker, blocked) do
1236 {:ok, blocker, _} = unfollow(blocker, blocked)
1237 blocker
1238 else
1239 blocker
1240 end
1241
1242 # clear any requested follows as well
1243 blocked =
1244 case CommonAPI.reject_follow_request(blocked, blocker) do
1245 {:ok, %User{} = updated_blocked} -> updated_blocked
1246 nil -> blocked
1247 end
1248
1249 unsubscribe(blocked, blocker)
1250
1251 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1252
1253 {:ok, blocker} = update_follower_count(blocker)
1254 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1255 add_to_block(blocker, blocked)
1256 end
1257
1258 # helper to handle the block given only an actor's AP id
1259 def block(%User{} = blocker, %{ap_id: ap_id}) do
1260 block(blocker, get_cached_by_ap_id(ap_id))
1261 end
1262
1263 def unblock(%User{} = blocker, %User{} = blocked) do
1264 remove_from_block(blocker, blocked)
1265 end
1266
1267 # helper to handle the block given only an actor's AP id
1268 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1269 unblock(blocker, get_cached_by_ap_id(ap_id))
1270 end
1271
1272 def mutes?(nil, _), do: false
1273 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1274
1275 def mutes_user?(%User{} = user, %User{} = target) do
1276 UserRelationship.mute_exists?(user, target)
1277 end
1278
1279 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1280 def muted_notifications?(nil, _), do: false
1281
1282 def muted_notifications?(%User{} = user, %User{} = target),
1283 do: UserRelationship.notification_mute_exists?(user, target)
1284
1285 def blocks?(nil, _), do: false
1286
1287 def blocks?(%User{} = user, %User{} = target) do
1288 blocks_user?(user, target) ||
1289 (blocks_domain?(user, target) and not User.following?(user, target))
1290 end
1291
1292 def blocks_user?(%User{} = user, %User{} = target) do
1293 UserRelationship.block_exists?(user, target)
1294 end
1295
1296 def blocks_user?(_, _), do: false
1297
1298 def blocks_domain?(%User{} = user, %User{} = target) do
1299 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1300 %{host: host} = URI.parse(target.ap_id)
1301 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1302 end
1303
1304 def blocks_domain?(_, _), do: false
1305
1306 def subscribed_to?(%User{} = user, %User{} = target) do
1307 # Note: the relationship is inverse: subscriber acts as relationship target
1308 UserRelationship.inverse_subscription_exists?(target, user)
1309 end
1310
1311 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1312 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1313 subscribed_to?(user, target)
1314 end
1315 end
1316
1317 @doc """
1318 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1319 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1320 """
1321 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1322 def outgoing_relationships_ap_ids(_user, []), do: %{}
1323
1324 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1325
1326 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1327 when is_list(relationship_types) do
1328 db_result =
1329 user
1330 |> assoc(:outgoing_relationships)
1331 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1332 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1333 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1334 |> group_by([user_rel, u], user_rel.relationship_type)
1335 |> Repo.all()
1336 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1337
1338 Enum.into(
1339 relationship_types,
1340 %{},
1341 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1342 )
1343 end
1344
1345 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1346
1347 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1348
1349 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1350
1351 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1352 when is_list(relationship_types) do
1353 user
1354 |> assoc(:incoming_relationships)
1355 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1356 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1357 |> maybe_filter_on_ap_id(ap_ids)
1358 |> select([user_rel, u], u.ap_id)
1359 |> distinct(true)
1360 |> Repo.all()
1361 end
1362
1363 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1364 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1365 end
1366
1367 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1368
1369 def deactivate_async(user, status \\ true) do
1370 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1371 end
1372
1373 def deactivate(user, status \\ true)
1374
1375 def deactivate(users, status) when is_list(users) do
1376 Repo.transaction(fn ->
1377 for user <- users, do: deactivate(user, status)
1378 end)
1379 end
1380
1381 def deactivate(%User{} = user, status) do
1382 with {:ok, user} <- set_activation_status(user, status) do
1383 user
1384 |> get_followers()
1385 |> Enum.filter(& &1.local)
1386 |> Enum.each(fn follower ->
1387 follower |> update_following_count() |> set_cache()
1388 end)
1389
1390 # Only update local user counts, remote will be update during the next pull.
1391 user
1392 |> get_friends()
1393 |> Enum.filter(& &1.local)
1394 |> Enum.each(&update_follower_count/1)
1395
1396 {:ok, user}
1397 end
1398 end
1399
1400 def update_notification_settings(%User{} = user, settings) do
1401 user
1402 |> cast(%{notification_settings: settings}, [])
1403 |> cast_embed(:notification_settings)
1404 |> validate_required([:notification_settings])
1405 |> update_and_set_cache()
1406 end
1407
1408 def delete(users) when is_list(users) do
1409 for user <- users, do: delete(user)
1410 end
1411
1412 def delete(%User{} = user) do
1413 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1414 end
1415
1416 def perform(:force_password_reset, user), do: force_password_reset(user)
1417
1418 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1419 def perform(:delete, %User{} = user) do
1420 {:ok, _user} = ActivityPub.delete(user)
1421
1422 # Remove all relationships
1423 user
1424 |> get_followers()
1425 |> Enum.each(fn follower ->
1426 ActivityPub.unfollow(follower, user)
1427 unfollow(follower, user)
1428 end)
1429
1430 user
1431 |> get_friends()
1432 |> Enum.each(fn followed ->
1433 ActivityPub.unfollow(user, followed)
1434 unfollow(user, followed)
1435 end)
1436
1437 delete_user_activities(user)
1438 invalidate_cache(user)
1439 Repo.delete(user)
1440 end
1441
1442 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1443
1444 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1445 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1446 when is_list(blocked_identifiers) do
1447 Enum.map(
1448 blocked_identifiers,
1449 fn blocked_identifier ->
1450 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1451 {:ok, _user_block} <- block(blocker, blocked),
1452 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1453 blocked
1454 else
1455 err ->
1456 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1457 err
1458 end
1459 end
1460 )
1461 end
1462
1463 def perform(:follow_import, %User{} = follower, followed_identifiers)
1464 when is_list(followed_identifiers) do
1465 Enum.map(
1466 followed_identifiers,
1467 fn followed_identifier ->
1468 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1469 {:ok, follower} <- maybe_direct_follow(follower, followed),
1470 {:ok, _} <- ActivityPub.follow(follower, followed) do
1471 followed
1472 else
1473 err ->
1474 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1475 err
1476 end
1477 end
1478 )
1479 end
1480
1481 @spec external_users_query() :: Ecto.Query.t()
1482 def external_users_query do
1483 User.Query.build(%{
1484 external: true,
1485 active: true,
1486 order_by: :id
1487 })
1488 end
1489
1490 @spec external_users(keyword()) :: [User.t()]
1491 def external_users(opts \\ []) do
1492 query =
1493 external_users_query()
1494 |> select([u], struct(u, [:id, :ap_id]))
1495
1496 query =
1497 if opts[:max_id],
1498 do: where(query, [u], u.id > ^opts[:max_id]),
1499 else: query
1500
1501 query =
1502 if opts[:limit],
1503 do: limit(query, ^opts[:limit]),
1504 else: query
1505
1506 Repo.all(query)
1507 end
1508
1509 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1510 BackgroundWorker.enqueue("blocks_import", %{
1511 "blocker_id" => blocker.id,
1512 "blocked_identifiers" => blocked_identifiers
1513 })
1514 end
1515
1516 def follow_import(%User{} = follower, followed_identifiers)
1517 when is_list(followed_identifiers) do
1518 BackgroundWorker.enqueue("follow_import", %{
1519 "follower_id" => follower.id,
1520 "followed_identifiers" => followed_identifiers
1521 })
1522 end
1523
1524 def delete_user_activities(%User{ap_id: ap_id}) do
1525 ap_id
1526 |> Activity.Queries.by_actor()
1527 |> RepoStreamer.chunk_stream(50)
1528 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1529 |> Stream.run()
1530 end
1531
1532 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1533 activity
1534 |> Object.normalize()
1535 |> ActivityPub.delete()
1536 end
1537
1538 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1539 object = Object.normalize(activity)
1540
1541 activity.actor
1542 |> get_cached_by_ap_id()
1543 |> ActivityPub.unlike(object)
1544 end
1545
1546 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1547 object = Object.normalize(activity)
1548
1549 activity.actor
1550 |> get_cached_by_ap_id()
1551 |> ActivityPub.unannounce(object)
1552 end
1553
1554 defp delete_activity(_activity), do: "Doing nothing"
1555
1556 def html_filter_policy(%User{no_rich_text: true}) do
1557 Pleroma.HTML.Scrubber.TwitterText
1558 end
1559
1560 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1561
1562 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1563
1564 def get_or_fetch_by_ap_id(ap_id) do
1565 user = get_cached_by_ap_id(ap_id)
1566
1567 if !is_nil(user) and !needs_update?(user) do
1568 {:ok, user}
1569 else
1570 fetch_by_ap_id(ap_id)
1571 end
1572 end
1573
1574 @doc """
1575 Creates an internal service actor by URI if missing.
1576 Optionally takes nickname for addressing.
1577 """
1578 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1579 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1580 {_, user} =
1581 case get_cached_by_ap_id(uri) do
1582 nil ->
1583 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1584 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1585 {:error, nil}
1586 end
1587
1588 %User{invisible: false} = user ->
1589 set_invisible(user)
1590
1591 user ->
1592 {:ok, user}
1593 end
1594
1595 user
1596 end
1597
1598 @spec set_invisible(User.t()) :: {:ok, User.t()}
1599 defp set_invisible(user) do
1600 user
1601 |> change(%{invisible: true})
1602 |> update_and_set_cache()
1603 end
1604
1605 @spec create_service_actor(String.t(), String.t()) ::
1606 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1607 defp create_service_actor(uri, nickname) do
1608 %User{
1609 invisible: true,
1610 local: true,
1611 ap_id: uri,
1612 nickname: nickname,
1613 follower_address: uri <> "/followers"
1614 }
1615 |> change
1616 |> unique_constraint(:nickname)
1617 |> Repo.insert()
1618 |> set_cache()
1619 end
1620
1621 # AP style
1622 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1623 key =
1624 public_key_pem
1625 |> :public_key.pem_decode()
1626 |> hd()
1627 |> :public_key.pem_entry_decode()
1628
1629 {:ok, key}
1630 end
1631
1632 def public_key(_), do: {:error, "not found key"}
1633
1634 def get_public_key_for_ap_id(ap_id) do
1635 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1636 {:ok, public_key} <- public_key(user) do
1637 {:ok, public_key}
1638 else
1639 _ -> :error
1640 end
1641 end
1642
1643 defp blank?(""), do: nil
1644 defp blank?(n), do: n
1645
1646 def insert_or_update_user(data) do
1647 data
1648 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1649 |> remote_user_creation()
1650 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1651 |> set_cache()
1652 end
1653
1654 def ap_enabled?(%User{local: true}), do: true
1655 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1656 def ap_enabled?(_), do: false
1657
1658 @doc "Gets or fetch a user by uri or nickname."
1659 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1660 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1661 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1662
1663 # wait a period of time and return newest version of the User structs
1664 # this is because we have synchronous follow APIs and need to simulate them
1665 # with an async handshake
1666 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1667 with %User{} = a <- get_cached_by_id(a.id),
1668 %User{} = b <- get_cached_by_id(b.id) do
1669 {:ok, a, b}
1670 else
1671 nil -> :error
1672 end
1673 end
1674
1675 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1676 with :ok <- :timer.sleep(timeout),
1677 %User{} = a <- get_cached_by_id(a.id),
1678 %User{} = b <- get_cached_by_id(b.id) do
1679 {:ok, a, b}
1680 else
1681 nil -> :error
1682 end
1683 end
1684
1685 def parse_bio(bio) when is_binary(bio) and bio != "" do
1686 bio
1687 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1688 |> elem(0)
1689 end
1690
1691 def parse_bio(_), do: ""
1692
1693 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1694 # TODO: get profile URLs other than user.ap_id
1695 profile_urls = [user.ap_id]
1696
1697 bio
1698 |> CommonUtils.format_input("text/plain",
1699 mentions_format: :full,
1700 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1701 )
1702 |> elem(0)
1703 end
1704
1705 def parse_bio(_, _), do: ""
1706
1707 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1708 Repo.transaction(fn ->
1709 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1710 end)
1711 end
1712
1713 def tag(nickname, tags) when is_binary(nickname),
1714 do: tag(get_by_nickname(nickname), tags)
1715
1716 def tag(%User{} = user, tags),
1717 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1718
1719 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1720 Repo.transaction(fn ->
1721 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1722 end)
1723 end
1724
1725 def untag(nickname, tags) when is_binary(nickname),
1726 do: untag(get_by_nickname(nickname), tags)
1727
1728 def untag(%User{} = user, tags),
1729 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1730
1731 defp update_tags(%User{} = user, new_tags) do
1732 {:ok, updated_user} =
1733 user
1734 |> change(%{tags: new_tags})
1735 |> update_and_set_cache()
1736
1737 updated_user
1738 end
1739
1740 defp normalize_tags(tags) do
1741 [tags]
1742 |> List.flatten()
1743 |> Enum.map(&String.downcase/1)
1744 end
1745
1746 defp local_nickname_regex do
1747 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1748 @extended_local_nickname_regex
1749 else
1750 @strict_local_nickname_regex
1751 end
1752 end
1753
1754 def local_nickname(nickname_or_mention) do
1755 nickname_or_mention
1756 |> full_nickname()
1757 |> String.split("@")
1758 |> hd()
1759 end
1760
1761 def full_nickname(nickname_or_mention),
1762 do: String.trim_leading(nickname_or_mention, "@")
1763
1764 def error_user(ap_id) do
1765 %User{
1766 name: ap_id,
1767 ap_id: ap_id,
1768 nickname: "erroruser@example.com",
1769 inserted_at: NaiveDateTime.utc_now()
1770 }
1771 end
1772
1773 @spec all_superusers() :: [User.t()]
1774 def all_superusers do
1775 User.Query.build(%{super_users: true, local: true, deactivated: false})
1776 |> Repo.all()
1777 end
1778
1779 def muting_reblogs?(%User{} = user, %User{} = target) do
1780 UserRelationship.reblog_mute_exists?(user, target)
1781 end
1782
1783 def showing_reblogs?(%User{} = user, %User{} = target) do
1784 not muting_reblogs?(user, target)
1785 end
1786
1787 @doc """
1788 The function returns a query to get users with no activity for given interval of days.
1789 Inactive users are those who didn't read any notification, or had any activity where
1790 the user is the activity's actor, during `inactivity_threshold` days.
1791 Deactivated users will not appear in this list.
1792
1793 ## Examples
1794
1795 iex> Pleroma.User.list_inactive_users()
1796 %Ecto.Query{}
1797 """
1798 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1799 def list_inactive_users_query(inactivity_threshold \\ 7) do
1800 negative_inactivity_threshold = -inactivity_threshold
1801 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1802 # Subqueries are not supported in `where` clauses, join gets too complicated.
1803 has_read_notifications =
1804 from(n in Pleroma.Notification,
1805 where: n.seen == true,
1806 group_by: n.id,
1807 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1808 select: n.user_id
1809 )
1810 |> Pleroma.Repo.all()
1811
1812 from(u in Pleroma.User,
1813 left_join: a in Pleroma.Activity,
1814 on: u.ap_id == a.actor,
1815 where: not is_nil(u.nickname),
1816 where: u.deactivated != ^true,
1817 where: u.id not in ^has_read_notifications,
1818 group_by: u.id,
1819 having:
1820 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1821 is_nil(max(a.inserted_at))
1822 )
1823 end
1824
1825 @doc """
1826 Enable or disable email notifications for user
1827
1828 ## Examples
1829
1830 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1831 Pleroma.User{email_notifications: %{"digest" => true}}
1832
1833 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1834 Pleroma.User{email_notifications: %{"digest" => false}}
1835 """
1836 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1837 {:ok, t()} | {:error, Ecto.Changeset.t()}
1838 def switch_email_notifications(user, type, status) do
1839 User.update_email_notifications(user, %{type => status})
1840 end
1841
1842 @doc """
1843 Set `last_digest_emailed_at` value for the user to current time
1844 """
1845 @spec touch_last_digest_emailed_at(t()) :: t()
1846 def touch_last_digest_emailed_at(user) do
1847 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1848
1849 {:ok, updated_user} =
1850 user
1851 |> change(%{last_digest_emailed_at: now})
1852 |> update_and_set_cache()
1853
1854 updated_user
1855 end
1856
1857 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1858 def toggle_confirmation(%User{} = user) do
1859 user
1860 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1861 |> update_and_set_cache()
1862 end
1863
1864 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1865 def toggle_confirmation(users) do
1866 Enum.map(users, &toggle_confirmation/1)
1867 end
1868
1869 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1870 mascot
1871 end
1872
1873 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1874 # use instance-default
1875 config = Pleroma.Config.get([:assets, :mascots])
1876 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1877 mascot = Keyword.get(config, default_mascot)
1878
1879 %{
1880 "id" => "default-mascot",
1881 "url" => mascot[:url],
1882 "preview_url" => mascot[:url],
1883 "pleroma" => %{
1884 "mime_type" => mascot[:mime_type]
1885 }
1886 }
1887 end
1888
1889 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1890
1891 def ensure_keys_present(%User{} = user) do
1892 with {:ok, pem} <- Keys.generate_rsa_pem() do
1893 user
1894 |> cast(%{keys: pem}, [:keys])
1895 |> validate_required([:keys])
1896 |> update_and_set_cache()
1897 end
1898 end
1899
1900 def get_ap_ids_by_nicknames(nicknames) do
1901 from(u in User,
1902 where: u.nickname in ^nicknames,
1903 select: u.ap_id
1904 )
1905 |> Repo.all()
1906 end
1907
1908 defdelegate search(query, opts \\ []), to: User.Search
1909
1910 defp put_password_hash(
1911 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1912 ) do
1913 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1914 end
1915
1916 defp put_password_hash(changeset), do: changeset
1917
1918 def is_internal_user?(%User{nickname: nil}), do: true
1919 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1920 def is_internal_user?(_), do: false
1921
1922 # A hack because user delete activities have a fake id for whatever reason
1923 # TODO: Get rid of this
1924 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1925
1926 def get_delivered_users_by_object_id(object_id) do
1927 from(u in User,
1928 inner_join: delivery in assoc(u, :deliveries),
1929 where: delivery.object_id == ^object_id
1930 )
1931 |> Repo.all()
1932 end
1933
1934 def change_email(user, email) do
1935 user
1936 |> cast(%{email: email}, [:email])
1937 |> validate_required([:email])
1938 |> unique_constraint(:email)
1939 |> validate_format(:email, @email_regex)
1940 |> update_and_set_cache()
1941 end
1942
1943 # Internal function; public one is `deactivate/2`
1944 defp set_activation_status(user, deactivated) do
1945 user
1946 |> cast(%{deactivated: deactivated}, [:deactivated])
1947 |> update_and_set_cache()
1948 end
1949
1950 def update_banner(user, banner) do
1951 user
1952 |> cast(%{banner: banner}, [:banner])
1953 |> update_and_set_cache()
1954 end
1955
1956 def update_background(user, background) do
1957 user
1958 |> cast(%{background: background}, [:background])
1959 |> update_and_set_cache()
1960 end
1961
1962 def update_source_data(user, source_data) do
1963 user
1964 |> cast(%{source_data: source_data}, [:source_data])
1965 |> update_and_set_cache()
1966 end
1967
1968 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1969 %{
1970 admin: is_admin,
1971 moderator: is_moderator
1972 }
1973 end
1974
1975 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1976 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1977 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1978 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1979
1980 attachment
1981 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1982 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1983 |> Enum.take(limit)
1984 end
1985
1986 def fields(%{fields: nil}), do: []
1987
1988 def fields(%{fields: fields}), do: fields
1989
1990 def sanitized_fields(%User{} = user) do
1991 user
1992 |> User.fields()
1993 |> Enum.map(fn %{"name" => name, "value" => value} ->
1994 %{
1995 "name" => name,
1996 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1997 }
1998 end)
1999 end
2000
2001 def validate_fields(changeset, remote? \\ false) do
2002 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2003 limit = Pleroma.Config.get([:instance, limit_name], 0)
2004
2005 changeset
2006 |> validate_length(:fields, max: limit)
2007 |> validate_change(:fields, fn :fields, fields ->
2008 if Enum.all?(fields, &valid_field?/1) do
2009 []
2010 else
2011 [fields: "invalid"]
2012 end
2013 end)
2014 end
2015
2016 defp valid_field?(%{"name" => name, "value" => value}) do
2017 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2018 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2019
2020 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2021 String.length(value) <= value_limit
2022 end
2023
2024 defp valid_field?(_), do: false
2025
2026 defp truncate_field(%{"name" => name, "value" => value}) do
2027 {name, _chopped} =
2028 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2029
2030 {value, _chopped} =
2031 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2032
2033 %{"name" => name, "value" => value}
2034 end
2035
2036 def admin_api_update(user, params) do
2037 user
2038 |> cast(params, [
2039 :is_moderator,
2040 :is_admin,
2041 :show_role
2042 ])
2043 |> update_and_set_cache()
2044 end
2045
2046 @doc "Signs user out of all applications"
2047 def global_sign_out(user) do
2048 OAuth.Authorization.delete_user_authorizations(user)
2049 OAuth.Token.delete_user_tokens(user)
2050 end
2051
2052 def mascot_update(user, url) do
2053 user
2054 |> cast(%{mascot: url}, [:mascot])
2055 |> validate_required([:mascot])
2056 |> update_and_set_cache()
2057 end
2058
2059 def mastodon_settings_update(user, settings) do
2060 user
2061 |> cast(%{settings: settings}, [:settings])
2062 |> validate_required([:settings])
2063 |> update_and_set_cache()
2064 end
2065
2066 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2067 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2068 params =
2069 if need_confirmation? do
2070 %{
2071 confirmation_pending: true,
2072 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2073 }
2074 else
2075 %{
2076 confirmation_pending: false,
2077 confirmation_token: nil
2078 }
2079 end
2080
2081 cast(user, params, [:confirmation_pending, :confirmation_token])
2082 end
2083
2084 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2085 if id not in user.pinned_activities do
2086 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2087 params = %{pinned_activities: user.pinned_activities ++ [id]}
2088
2089 user
2090 |> cast(params, [:pinned_activities])
2091 |> validate_length(:pinned_activities,
2092 max: max_pinned_statuses,
2093 message: "You have already pinned the maximum number of statuses"
2094 )
2095 else
2096 change(user)
2097 end
2098 |> update_and_set_cache()
2099 end
2100
2101 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2102 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2103
2104 user
2105 |> cast(params, [:pinned_activities])
2106 |> update_and_set_cache()
2107 end
2108
2109 def update_email_notifications(user, settings) do
2110 email_notifications =
2111 user.email_notifications
2112 |> Map.merge(settings)
2113 |> Map.take(["digest"])
2114
2115 params = %{email_notifications: email_notifications}
2116 fields = [:email_notifications]
2117
2118 user
2119 |> cast(params, fields)
2120 |> validate_required(fields)
2121 |> update_and_set_cache()
2122 end
2123
2124 defp set_domain_blocks(user, domain_blocks) do
2125 params = %{domain_blocks: domain_blocks}
2126
2127 user
2128 |> cast(params, [:domain_blocks])
2129 |> validate_required([:domain_blocks])
2130 |> update_and_set_cache()
2131 end
2132
2133 def block_domain(user, domain_blocked) do
2134 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2135 end
2136
2137 def unblock_domain(user, domain_blocked) do
2138 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2139 end
2140
2141 @spec add_to_block(User.t(), User.t()) ::
2142 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2143 defp add_to_block(%User{} = user, %User{} = blocked) do
2144 UserRelationship.create_block(user, blocked)
2145 end
2146
2147 @spec add_to_block(User.t(), User.t()) ::
2148 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2149 defp remove_from_block(%User{} = user, %User{} = blocked) do
2150 UserRelationship.delete_block(user, blocked)
2151 end
2152
2153 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2154 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2155 {:ok, user_notification_mute} <-
2156 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2157 {:ok, nil} do
2158 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2159 end
2160 end
2161
2162 defp remove_from_mutes(user, %User{} = muted_user) do
2163 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2164 {:ok, user_notification_mute} <-
2165 UserRelationship.delete_notification_mute(user, muted_user) do
2166 {:ok, [user_mute, user_notification_mute]}
2167 end
2168 end
2169
2170 def set_invisible(user, invisible) do
2171 params = %{invisible: invisible}
2172
2173 user
2174 |> cast(params, [:invisible])
2175 |> validate_required([:invisible])
2176 |> update_and_set_cache()
2177 end
2178
2179 def sanitize_html(%User{} = user) do
2180 sanitize_html(user, nil)
2181 end
2182
2183 # User data that mastodon isn't filtering (treated as plaintext):
2184 # - field name
2185 # - display name
2186 def sanitize_html(%User{} = user, filter) do
2187 fields =
2188 user
2189 |> User.fields()
2190 |> Enum.map(fn %{"name" => name, "value" => value} ->
2191 %{
2192 "name" => name,
2193 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2194 }
2195 end)
2196
2197 user
2198 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2199 |> Map.put(:fields, fields)
2200 end
2201 end